3

I've a SQL Server database where the decimal values are stored ALWAYS with the "." as the decimal separator.

I've an ASP.NET MVC 5 application that uses the culture of the user. I'm testing the app with the "es-ES" culture where the decimal separator is "," when I try to get some values from a table having decimal values it's returning the wrong values.

e.g.

class Model
{
   public string Description {get; set;}
   public decimal Length {get; set;}
}

values in the DB

<table>
  <th>Description</th>
  <th>Length</th>
  <tr>
    <td>Dog</td>
    <td>4.25</td>
  </tr>
  <tr>
    <td>Cat</td>
    <td>2.11</td>
  </tr>
</table>

When I run this query

var result = 
   from x in DbContext.Model
   select x;

I get 425 instead 4.25 and 211 instead 2.11

How can I tell to LINQ to use the invariant culture?

This is just a snippet to show what I need. In the real scenario I do something like this

var result = 
   from x in DbContext.Model
   select new 
   {
      Description = x.Description,
      Length = x.Length * 2
   };
vcRobe
  • 1,671
  • 3
  • 17
  • 35
  • wouldn't this be matter of setting `currentthread.currentculture = ...` to spanish? Basically, if user sends you data based on spanish culture, like `4,5`, your thread in MVC request needs to run spanish culture – T.S. Oct 26 '17 at 19:31
  • you're right I'm forcing the thread to change the culture to test other cultures than English. What's wrong with that? – vcRobe Oct 26 '17 at 19:36
  • What do you mean by, *"What's wrong with that"*? Nothing wrong. the problem starts when your server runs specific culture, like "en-US" but you retrieve text data, like JSON from computer that runs something different. So the server thread doesn't know how to convert text `4,5` into `decimal`. But if you set server thread to culture that matches endpoint user's culture, you get it converted fine. – T.S. Oct 26 '17 at 19:41
  • Sorry I didn't read the whole comment. I'm forcing the app to change the thread's culture to Spanish and I set a breakpoint in the action that runs the LINQ to verify the thread's culture and it's Spanish so what can be wrong? – vcRobe Oct 26 '17 at 19:50
  • There is disconnect somewhere. check this https://stackoverflow.com/questions/7216057/setting-culture-for-asp-net-mvc-application-on-vs-dev-server-and-iis – T.S. Oct 26 '17 at 19:58

1 Answers1

2

You don't tell LINQ to use the invariant culture. LINQ is only a collection of extension functions of the IEnumerable<T> and IQueryable<T> interfaces.

You are using a DbContext. It seems this is an Entity Framework DbContext, but even it is not, your DbContext is supposed to hide from you in what format numbers are stored in the storage (which is usually a SQL database, but doesn't have to be, it can be a CSV-file, EXCEL, JSon, whatever).

If you look closely to your object DbContext.Model, you'll see that the class of this object implements IQueryable<Model>.

This means, that this object can hold two things: an Expression and a 'Provider'. (ok, also an elementType, in your example this will be Model, not meaningful in this discussion)

When you use DbContext.Model as the starting point of your query, you are in fact changing the Expression inside the IQueryable<Model>. As long as you are not asking for the first element in of your sequence of models the Provider is not used.

The Provider hides from you how your sequence of Models is stored. The Provider knows whether it is a relational database, or a CSV-file, or whatever. It is the task of the Provider to translate the Expression into a format that the storage understands and fetch the data from the storage.

For Entity Framework DbContexts, this usually means that the storage is a SQL(-like) database. The Provider knows the database model and it knows how to translate your Expression into SQL and how to communicate with your database.

After reading this, you should understand, that the user of you DbContext.Model object should not be aware in what format the storage saves decimals. It is the task of the Provider to translate decimals into storage format. And if this storage format stores decimals as Spanish strings, then the Provider should translate decimals into Spanish strings.

Now back to your question, because this knowledge does not solve your problem.

You should investigate why the Spanish notation seeps through to your DbContext.Models

It seems that the storage of your database was told to store Length as a string instead of a decimal.

If your database would be SQL, then a decimal would not have been saved as a string, but as a value with a certain precision.

If the database saves your Length as a string, then the designer of your database told entity framework that Model.Length is a string, not a decimal. In that case, the definition of your Model class does not correctly represent the contents of your Model table in the database. Model.Length should not be a decimal, but a string, thus representing the structure of your database Model table

The neat solution would be to change the Length column in your database from string to value-with-precision (whatever database you are using). If you can't change the structure of the Model table you database, you should change your Model class such that it represents your model:

class Model
{
    public string Description {get; set;}
    public string Length {get; set;}

    private static IFormatProvider spanishCulture = CultureInfo.CreateSpecificCulture("es-ES");

    [NotMapped]
    public decimal LengthInMeters
    {
        get {return Decimal.Parse(this.Length, spanishCulture);}
        set {this.Length = value.ToString(spanishCulture); }
    }
}

This would also solve the problem of the unknown unit of length (m? cm? inch?)

An even cleaner solution, where you let your Model class exactly represent the database table, would be to create a SpanishModel class and create extension functions to convert from Model to SpanishModel and back

// model represents exactly database table:
class Model
{
    public string Description {get; set;}
    public string Length {get; set;}
}

class SpanishModel
{
    public string Description {get; set;}
    public decimal Length {get; set;}
}

// extension functions of model:
static class ModelExtensions
{
    private static IFormatProvider spanishCulture = CultureInfo.CreateSpecificCulture("es-ES");

    public static SpanishModel AsSpanishModel(this Model model)
    {
        return new SpanishModel()
        {
            Description = model.Description,
            Length = Decimal.Parse(model.Length, spanishCulture);
        }
    }
    public static Model AsModel(this SpanishModel model) 
    {
         return new Model()
         {
             description = model.Description,
             Length = model.Length.ToString(spanishCulture),
         };
    }

    public static IEnumerable<SpanishModel> AsSpanishModels(this IEnumerable<Model> models)
    {
         return models.Select(model => model.AsSpanishModel();
    }

     public static IEnumerable<SpanishModel> AsModels(this IEnumerable<SpanishModel> models)
    {
         return models.Select(model => model.AsModel();
    }
}

Usage:

IEnumerable<SpanishModel> requestedModels = myDbContext.Models
    .Where(model => model.Description == ...)
    .AsSpanishModels();

See Extension Functions Demystified

If you really want to hide the fact that SpanishModels are not part of your database, consider extending your DbContext class:

static class MyDbContextExtensions
{
    public static IQueryable<SpanishModel> SpanishModel(this MyDbContext dbContext)
    {
         return dbContext.Models.AsSpanishModels();
    }
}

usage:

using (var myDbContext = new MyDbContext(...))
{
    IQueryable<SpanishModel> tallSpanishModels = myDbContext.SpanishModels
        .Where(model => model.Length > 1.85M);
    foreach (SpanishModel tallModel in tallSpanishModels)
    {
        Console.WriteLine($"{tallModel.Description} has a length of {tallModel.Length}";
    }
}

You almost can't see any difference between other DbSets of your DbContext and your SpanishModel. Come to think of it, I think this is the neatest solution.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116