I ran into an issue using value conversion, which is new in EF Core 2.1, for List to string conversion.
Since I did not need to be able to filter the List of Enum values in the Database I decided to map my List of Enum Values to a comma delimited string with the int values.
The Conversion should look like this:
From: List<EnumType>{EnumType.Value1, EnumType.Value2}
To: 1,2
Everything seemed to work fine but EF seems not to notice that the list of Enum values was changed and does not issue an update in the database. Is there a limitation that does not allow value conversion for lists?
The Value Conversion Code looks like this:
private const char ENUM_LIST_DELIMITER = ',';
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Entity>().Property(x => x.Types)
.HasConversion(x => ConvertToString(x), x => ConvertToEnumList<EnumType>(x));
}
private static List<TEnum> ConvertToEnumList<TEnum>(string value) where TEnum : struct, IConvertible
{
return value?.Split(ENUM_LIST_DELIMITER)
.Select(Enum.Parse<TEnum>)
.ToList();
}
private static string ConvertToString<TEnum>(IEnumerable<TEnum> value) where TEnum : struct, IConvertible
{
return string.Join(ENUM_LIST_DELIMITER, value.Select(x => Convert.ToInt32(x)));
}
}
Value conversion on List<T> worked for me when using DbContext.Set<T>.Update(entity) instead of getting the entity from the context, changing some properties and then calling .SaveChangesAsync(). This is probably due to what #Ivan Stoev said regarding equality comparison. Update() marks all properties in an entity as changed. Perhaps not desired in all situations, but can be an quick fix.
Related
I am trying to figure out how to enable two-way name translation between postgres DB and EF Core. I have found a link where it is told how values are translated from EF Core to DB, but nothing about from DB to EF Core. The problem is that when I read from DB I get values in snake case, but I need them in pascal case.
How are you reading your data from the DB?
Providing you are writing from and reading into the C# enum type that corresponds to the postgres enum type, Npgsql takes care of translating the values both ways.
For example:
Define your types:
public enum SomeDbEnum
{
FirstValue,
SecondValue
}
public class SomeDbObject
{
public SomeDbEnum DbEnum { get; set; }
}
Map your enum types
protected override void OnModelCreating(ModelBuilder builder)
=> builder.HasPostgresEnum<SomeDbEnum>();
static MyDbContext()
=> NpgsqlConnection.GlobalTypeMapper.MapEnum<SomeDbEnum>();
Try it out:
var toWriteToDb = new SomeDbObject { DbEnum = SomeDbEnum.SecondValue };
context.SomeDbObject.Add(toWriteToDb);
// value inserted as "second_value"
var readFromDb = context.SomeDbObject.FirstOrDefault(x => x.DbEnum == SomeDbEnum.SecondValue);
Console.WriteLine(readFromDb.DbEnum);
// value output as SecondValue
The same thing applies even if you use something other than the default naming translations, i.e. registered some other INpgsqlNameTranslator (other than the default NpgsqlSnakeCaseNameTranslator) or decorate your enum members with the PgNameAttribute.
Had to research myself, just use the NpgsqlNullNameTranslator. For some reason NpgSql uses NpgsqlSnakeCaseNameTranslator as default, at least for EF Core 7.0
modelBuilder.HasPostgresEnum<MyEnum>(nameTranslator: new NpgsqlNullNameTranslator());
EDIT: in order to save them as string in DB:
modelBuilder.Entity<MyEntity>()
.Property(u => u.EnumProperty)
.HasConversion(new EnumToStringConverter<MyEnum>());
I'm trying to write an extension method to include a certain property (text element, themselves containing a collection of translations) that are present in many of my entity models.
I had no problem with the .Include function:
public static IIncludableQueryable<T, IEnumerable<Translation>> IncludeTextBitWithTranslations<T>(this IQueryable<T> source, Expression<Func<T, TextBit>> predicate) where T: class
{
var result = source.Include(predicate).ThenInclude(t => t.Translations);
return result;
}
And tests proved successful.
Now, in some cases, I have entities that have all their texts in a child - for example Article entity has an ArticleInfo property that contains a few text elements. So I figure I just needed to do another extension that was a ThenInclude instead. With a few differences I finally get this :
public static IIncludableQueryable<TEntity, ICollection<Translation>> ThenIncludeTextBitWithTranslations<TEntity, TPreviousProperty, TextBit>(this IIncludableQueryable<TEntity, TPreviousProperty> source, Expression<Func<TPreviousProperty, TextBit>> predicate) where TEntity: class
{
var result = source.ThenInclude(predicate)
.ThenInclude(t => t.Translations);
return result;
}
And now I get this error:
'TextBit' does not contain a definition for 'Translations' and no extension method 'Translations' accepting an argument of 'TextBit' type was found
This error appears on the last lambda expression t => t.Translations.
This error is extremely weird for me, I've been looking all over the internet for some help on the matter but I was unsuccessful.
I tried forcing the type to the ThenInclude by adding them manually :
var result = source.ThenInclude(predicate)
.ThenInclude<TEntity, TextBit, ICollection<Translation>>(t => t.Translations);
but without success.
Does anyone have some clues as to why?
I'm very much at a loss here
You have extra type parameter TextBit in second one (ThenIncludeTextBitWithTranslations<TEntity, TPreviousProperty, TextBit>), so it is considered as a generic type, not an actual one, remove it:
public static IIncludableQueryable<TEntity, ICollection<Translation>> ThenIncludeTextBitWithTranslations<TEntity, TPreviousProperty>(this IIncludableQueryable<TEntity, TPreviousProperty> source, Expression<Func<TPreviousProperty, TextBit>> predicate) where TEntity: class
{
var result = source.ThenInclude(predicate).ThenInclude(t => t.Translations);
return result;
}
I have this value object
public class ProductReference : ValueObject
{
protected ProductReference(){}
public ProductReference(string value){}
public string Value{get; protected set;}
}
I use it in my entity as :
public class Product : Entity<long>
{
protected Product(){}
public ProductReference Reference{get; protected set;}
}
In the OnModelCreating of my DbContext I defined :
modelBuilder.Entity<Product>(entity => {
entity.Property(a => a.Reference)
.HasColumnName("Reference")
.HasConversion(
a => a.Value,
s => new ProductReference (s);
});
When I do :
await dbcontext.Products.Where(p=>p.Reference.Value.Contains("some text")).toArrayAsync();
I get an exception
Expression cannot be converted to a valid SQL statement
I know for sure there is a way to create a custom expression converter, but I cannot find a good, simple and EF Core 3.1 compatible example to deal with my issue and that explain me clearly the concepts I miss.
I found this very interesting project
https://github.com/StevenRasmussen/EFCore.SqlServer.NodaTime
but it is too advanced for me to reproduce it for only my use case.
[EDIT] the ValueObject ans Entity are from
CSharpFunctionalExtensions nuget package, I dont think they are really relevant in my question.
I am not completely sure if i understand correctly what you want to accomplish, but you could try to configure your ProductReference as an Owned Entity Type.
Here you would transform the following code from:
modelBuilder.Entity<Product>(entity => {
entity.Property(a => a.Reference)
.HasColumnName("Reference")
.HasConversion(
a => a.Value,
s => new ProductReference (s);
});
to
modelBuilder.Entity<Product>(entity => {
entity.OwnsOne(a => a.Reference, referenceBuilder => {
referenceBuilder.Property(p => p.Value).HasColumnName("Reference");
});
});
With that your select statement should work.
It could be that you have to play around with the properties of your class ProductReference, or use some modifiers of the fluent API.
So first for some context on what is happening here behind the scenes and why its not gonna work even for build in simple converters like BoolToZeroOneConverter.
The problem here is that you are calling when converting the new ProductReference(s). This is method where you can do whatever you want in it. For example if use it in a Select statement it will again fail. For example:
await dbcontext.Products
.Select(x=>new ProductReference(x.Value))
.toArrayAsync();
The reason is obvious, it won't be able to translate. But why it cant transform it to a query?
Because you are passing a constructor. Inside this constructor you could be doing API calls or using Reflections to set the variables to your object, pretty much anything. That of course is not able to be translated in an SQL query.
Converters are generally used for in memory but they can be used for databse operations as well. This would mean that you will need something like this:
await dbcontext.Products
.Select(x=>new ProductReference() // empty constructor that does nothing
{
Property1 = x.Property1 // I don't know how the constructor maps them
})
.toArrayAsync();
Using this type of expression allow you to actually transalte the expression to an SQL statement and not making the conversion on the SQL DB and not in memory.
Now in your specific case using:
.HasConversion(
a => a.Value,
s => new ProductReference (){};
});
Should fix your issues but I fail to understand why would you want to initialize or convert a ProductReference to a ProductReference.
I have an EF model with a notification emails property. The notification emails are saved in the database as string separated by ';'. I added a conversion to retrieve the data as a ICollection in the model. This is working well except one thing: when the string is null the collection is also null, and I want to convert it to an empty collection instead. is it possible?
//This is my code
entity.Property(e => e.NotificationEmails)
.HasConversion(
v => string.Join(",", v.Select(s => s.Trim())),
v => v.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries));
I tried to add String.IsNullOrEmpty(v) but EF ignores it.
Currently, it isn't possible :
https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions#configuring-a-value-converter
A null value will never be passed to a value converter. This makes the implementation of conversions easier and allows them to be shared amongst nullable and non-nullable properties.
It isn't elegant, but you can use a backing field :
public class Notification
{
private List<string> _emails = new List<string>();
public List<string> Emails
{
get => _emails;
set => _emails = value ?? new List<string>();
}
}
public class NotificationContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Notification>().Property(d => d.Emails).HasConversion(
v => string.Join(",", v.Select(s => s.Trim())),
v => v.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList()
);
modelBuilder.Entity<Notification>()
.Property(b => b.Emails)
.HasField("_emails")
.UsePropertyAccessMode(PropertyAccessMode.Property);
}
}
Note : in where, a empty list will not be translated by null, but by a empty string.
Edit : This feature is available from EF Core 6, but bugged.
See this comment :
For anyone watching this issue: there are significant problems when executing queries that either convert nulls in the database to non-nulls in code or vice-versa. Therefore, we have marked this feature as internal for EF Core 6.0. You can still use it, but you will get a compiler warning. The warning can be disabled with a #pragma.
I noticed a weird thing today when I tried to save an entity and return its id in EF Core
Before:
After:
I was thinking about if it was before calling saveChanges() but it works with another entity with a similar setup.
ps: I use unit of work to save all changes at the end.
What was the reason?
It will be negative until you save your changes. Just call Save on the context.
_dbContext.Locations.Add(location);
_dbContext.Save();
After the save, you will have the ID which is in the database. You can use transactions, which you can roll back in case there's a problem after you get the ID.
The other way would be not to use the database's built-in IDENTITY fields, but rather implement them yourself. This can be very useful when you have a lot of bulk insert operations, but it comes with a price — it's not easy to implement.
Apparently, this isn't a bug it's a feature: https://github.com/aspnet/EntityFrameworkCore/issues/6147
It's not too arduous to override this behavior like so:
public class IntValueGenerator : TemporaryNumberValueGenerator<int>
{
private int _current = 0;
public override int Next(EntityEntry entry)
{
return Interlocked.Increment(ref _current);
}
}
Then reference the custom value generator here:
public class CustomContext: DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var type in modelBuilder.Model.GetEntityTypes().Select(c => c.ClrType))
{
modelBuilder.Entity(type, b =>
{
b.Property("Id").HasValueGenerator<IntValueGenerator>();
});
}
base.OnModelCreating(modelBuilder);
}
}
This will result in temporary Id's that increment from "1", however, more effort is required to prevent key conflicts when attempting to save.