Can you create a generated ts vector column on a shadow property? - entity-framework-core

I'd like to create a generated tsvector column using npgsql / dotnet on a shadow property. I do not want to place the tsvector column in the entity class as I'd like to keep the domain entities agnostic of any specific database.
One way that may work is to use a shadow property for the tsvector column. I am able to create the tsvector column and index, but have not been able to figure out how to create generated column definition.
If the property exists in the domain entity, I can use the following:
builder
.HasGeneratedTsVectorColumn(
c => c.SearchVector,
"english",
c => new { c.Number, c.Name })
.HasIndex(p => p.SearchVector)
.HasMethod("GIN");
If the property is not in the domain, but rather a shadow property, I tried something like the foolowing:
builder
.Property<NpgsqlTsVector>("search_vector");
builder
.HasGeneratedTsVectorColumn(
c => EF.Property<NpgsqlTsVector>(c, "search_vector"),
"english",
c => new { c.ClientId, c.Name })
.HasIndex("search_vector")
.HasMethod("GIN");
Unfortunately, this does not work and, when generating the migrations, issues the following error:
---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
---> System.ArgumentException: The expression 'c => Property(c, "search_vector")' is not a valid member access expression. The expression should represent a simple property or field access: 't => t.MyProperty'. (Parameter 'memberAccessExpression')
Is it possible to create the definition of the generated column for a shadow property?
Of course, also interested if there are other ways to accomplish this besides using a shadow property.
Thanks for any help,
Eric

Related

EF Core throws NullReferenceException while comparing strings with CompareOptions

I have a Worker entity with string Name property, and just want to filter all Workers whose Name contains some specific string (from frontend text input).
When I do the filtering with:
_context.Workers.Where(w => w.Name.ToUpper().Contains(filter.ToUpper()).ToList()
it is working but it does not solves some specific diacritics in filter term.
When i try with:
var compareInfo = CultureInfo.InvariantCulture.CompareInfo;
_context.Workers.Where(w => compareInfo.IndexOf(w.Name, filter, CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase) > -1).ToList()
i get System.NullReferenceException: 'Object reference not set to an instance of an object.'
and console An exception occurred in the database while iterating the results of a query for context type 'Project.MyDbContext'.
I've also tried with
_context.Workers.Where(w => compareInfo.IndexOf((w.Name??""), filter, CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase) > -1).ToList()
to perform a null check, but same.
Did anyone have same problem, or maybe idea what could be changed here so I could accompiish searching with diacritics?
Thx!

Getting the column name (sspace) from an ospace property name

I can see from the following example how to get the table name of an OSpace type:
https://lowrymedia.com/2014/06/10/ef6-1-mapping-between-types-tables-including-derived-types/
But how do I go about getting the SSpace column name from an OSpace property name (i.e. CLR type property)?
By browsing the MetadataProperties from the corresponding CSpace property, I can see there is a "Configuration" entry containing the column name if changed using the Fluid API or ColumnAttribute, but the value of the entry is an internal class on EF's part. Is it at all possible?
I have browsed a few answers regarding this topic, but none of them take into account the Fluid API configuration.
P.S. the specific property I'm looking for is scalar, if that can simplify things...
Column Name
To get the column name, you have to first get the EdmProperty associated with that column in the “structural space” (SSpace). I provide code to do that below. Once you have the EdmProperty, the name of the column is simply EdmProperty.Name:
string GetColumnName(DbContext context, PropertyInfo property) {
return GetStructuralSpaceEdmProperty(context, property).Name;
}
Structural Space Property
This is based on an article. That article gives you enough information to map all the way to the structural EntityType. I added a bit at the end to do the actual property mapping to get the EdmProperty representing the column. As the article states, these APIs require ≥EntityFramework-6.1.
EdmProperty GetStructuralSpaceEdmProperty(DbContext context, PropertyInfo property) {
IObjectContextAdapter adapter = context;
var metadata = adapter.ObjectContext.MetadataWorkspace;
// First, you map the Object Space to the Conceptual Space.
var objectItemCollection = (ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace);
var objectEntityType = metadata.GetItems<EntityType>(DataSpace.OSpace)
.Single(oet => objectItemCollection.GetClrType(oet) == property.DeclaringType);
// Note: we are assuming that CSpace and OSpace name their properties the
// same instead of trying to use EF’s own OSSpace mappings here.
var conceptualEntityType = metadata.GetItems<EntityType>(DataSpace.CSpace)
.Single(cet => objectEntityType.Name == cet.Name);
var conceptualEdmProperty = conceptualEntityType.Properties
.Single(ep => ep.Name == property.Name);
// Then you map the conceptual space onto the structural space.
var entitySet = metadata.GetItems<EntityContainer>(DataSpace.CSpace)
.Single().EntitySets
.Single(es => es.ElementType.Name == conceptualEntityType.Name);
var entityMapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
.Single().EntitySetMappings
.Single(esm => esm.EntitySet == entitySet);
// The entity may be split to different tables or fragments.
var fragments = entityMapping.EntityTypeMappings
.SelectMany(etm => etm.Fragments);
var propertyMappings = fragments.SelectMany(f => f.PropertyMappings);
// Normal properties will be “ScalarPropertyMapping”.
// Depending on what information you are seeking or your
// model, you may be interested in other PropertyMapping.
var structuralSpaceProperty = propertyMappings
.OfType<ScalarPropertyMapping>()
.Single(pm => pm.Property == conceptualEdmProperty).Column;
return structuralSpaceProperty;
}
Note that once you have EdmProperty in structural space, there are a bunch of other useful properties you can read from it. For example, for SQL Server, EdmProperty.IsUnicode will be true for NVARCHAR/NCHAR and false for VARCHAR/CHAR types whereas this property is not set to a useful value in the conceptual space.
Random Information
Spaces in EF
Alex D. James’s blog post “Tip 10 — How to understand Entity Framework jargon” explains some of the terms of the API which do not make sense on their own. DataSpace.OSpace stands for “Object Space”, meaning the .net POD classes. DataSpace.SSpace stands for “Structural Space”, probably named after “structured” in the term “SQL” and thus meaning it most directly describes the backend database. DataSpace.CSpace stands for “Conceptual Space” which seems intended to be a neutral space which both the “Object Space” and “Structural Space” can map into. DataSpace.OCSpace stands for the mapping from the object space onto the conceptual space. We bypass this mapping because we assume that property names in the object space are the same as in our .net types. DataSpace.CSSpace stands for the mapping of conceptual space onto structural space. We use this mapping because the model may be configured to use a different column name via the fluent API or ColumnAttribute.
API Confusion
The metadata API of EF seems to assume that the consumer of the API has an understanding of the internals of EF to an extent. It is not made in a very type safe way which helps consumers. For example, the fact that we had to use Enumerable.OfType<TResult> to get to ScalarPropertyMapping means that one has to know to expect the collection to have ScalarPropertyMapping instances in it. Likewise, the MetadataWorkspace.GetItems<T>() method requires us to know that the sorts of items one would find in the metadata include EntityType. Thus, a deep understanding of the internals of EF or complete examples are necessary to write code that consumes the mapping portion of these APIs.

Entity Framework Code First: "The specified table 'foo' was not found in the model."

Haven't seen this error before, and a cursory web search turns up very little. Here's (I think) the offending code:
this.HasMany(a => a.ListItems).WithRequired()
.Map(m =>
{
m.MapKey("AttributeId");
m.ToTable("ProductAttributeListItem");
}
)
;
And here's the full error:
The specified table 'ProductAttributeListItem' was not found in the
model. Ensure that the table name has been correctly specified.
The table is there and spelled correctly.
The lack of search results makes me think I'm missing something obvious. What might that be?
If you want to define the table name of the entity ListItems is refering to you need to do that on the entity, not in the relationship mapping:
modelBuilder.Entity<ListItem>() // or whatever the entity is called
.ToTable("ProductAttributeListItem");
And remove m.ToTable from the Map action.

Custom display in CRUDified object

I have a an entity referencing another entity by MappedLongForeignKey.
I am using the CRUDify trait and have a problem.
Entity 1:
id
title (String)
validTo (Date)
Entity 2:
id
...
fk_entity_1 (mapped via MappedLongForeignKey)
In the listings generated by CRUDify for entity 2 I would like to include a column formatted as
fk_entity_1.title ( fk_entity_1.validTo )
I tried to create a function returning such a string and adding that fn to fieldsForDisplay, but it seems fieldsForDisplay requires mapped fields.
Is this possible to accomplish?
* Edit *
What I am trying to accomplish is (using the built in functionality of CRUDify for listings) produce listings as:
{Entity 2 fields} "Entity 1"
... Title_X (2001-01-01)
... Title_Y (2011-02-02)
If worse comes to worst, I can roll my own listings, but I really like the CRUDify functionality.
I'm not quite sure what you're trying to accomplish, but maybe you could override the asHtml method of the fields instead?

Including child Objects in Entityframework

I want to include child objects on an IQueryable list..
I want to include a child object on selected columns of some table type IQueryable list..
I tried like this:
IQueryable<Persons> persons = Context.Persons.Select(x=> new persons{Pkid=x.pkid, FirstName=x.FirstName}).AsQueryable();
persons= persons.Include("Address");
this include of child objects is not working..anyone please help...where I am doing wrong..
thanks alot...
Include doesn't work with projection. Moreover it is not needed. Just do this:
var query = context.Persons
.Select(x => new PersonDto
{
Id = x.pkid,
FirstName = x.FirstName,
Address = x.Address
});
Few points here:
No Include
Address accessed directly in projection, EF will handle this
I'm using PersonDto as target of projection. PersonDto has just Id, FirstName and Address.
You can project to custom type or anonymous type but you cannot project to entity type (the mapped type) - it doesn't work and it throws exception.
If you want to use mapped type you can't return only selected scalar columns - all columns will always be loaded. Only navigation properties can be loaded selectively. To overcome this people sometimes use Table splitting but that is something which works if you can divide your big entity into disjunct entities. In your scenario use just projection.
You cannot use Include() on a projection, try this:
Iquerable<Persons> persons = Context.Persons
.Include("Address")
.Select(x=> new persons{Pkid=x.pkid, FirstName=x.FirstName})
.AsQuerable();
Also you have a naming conflict, you project to a type persons and want to hold the results in an IQueryable named persons - one of them is wrong. Is there a reason you need the projection at all? You could just do
Iquerable<Persons> persons = Context.Persons.Include("Address");
First: Check if lazy loading is enabled or not. I experienced different results when it was enabled. I prefer lazy loading being disabled.
Second: Check this syntax:
result = (From person In context.Persons.Include("Address")).ToList();
P.S.: Useful EF Tips & Tricks : http://blogs.msdn.com/b/alexj/archive/2009/03/26/index-of-tips.aspx
UPDATE:
Include is not working, because your are using it on newly created objects, not the objects available in the context. you should use Include before creating new objects.
Check This:
result = (From person In context.Persons.Include("Address") Select New With {.FirstName = item.FirstName, .AddressValue = item.Address.Value}).ToList();