Can't do projections to Persistence Ignorance (POCO) class using EFPocoAdapter - c#

Given a simple POCO class such as:
public class User { public int Id { get; set; } public string Name { get; set; } public string Password { get; set; } }
When I try a query with projection to itself like so:
using (var context = new Context())
{
var user = context.User.Select(u => new User { Id = u.Id }).FirstOrDefault();
}
... I get:
Unhandled Exception: System.ArgumentException: 'Id' is not a member of type 'ORMTest1Model.Users'
... wich comes from the method ValidateMemberInitArgs(...) from System.Linq.Expressions.Expression (use Reflector).
In this method, the type from binding.Member.DeclaringType is of type PocoAdapters.UserAdapter (the generated one) and the type from variable "type" is of type User (the POCO class).
So... for some reason, it's mixing thing up.
Interestingly, if I create a class MyUser which is the exact copy of the poco class User, it works fine and both types at ValidateMemberInitArgs(...) are of type MyUser.
Can anyone reproduce the issue and shed a light on the solution?
Thanks!
(link to same question in project's discussion list: http://code.msdn.microsoft.com/EFPocoAdapter/Thread/View.aspx?ThreadId=2138)

I think the problem is how the class User is being resolved.
To confirm, can you refactor POCO class to be named PocoUser. Does the same error occur?
var user = context.User.Select(u => new User { Id = u.Id }).FirstOrDefault();
would become
var user = context.User.Select(u => new PocoUser { Id = u.Id }).FirstOrDefault();
HTH,
Dan

I think I figured out why this is happening.
MsSql allows for capital letters in table names, but MySql does not.
So if you create your poco adapter from an mssql entity model which contains capital letters in table names, and use it to query a mysql database, you will get this type of error.
In order to fix this, I just renamed my tables to all lowercase.

Related

Single Element Using Lookup operator

I am stuck in an issue with MongoDB. I am using C# driver for MongoDB.
So basically I am trying to join Categories document with Users document. Each category document has one property:
public Guid UserId { get; set; }
This property is an ObjectId behind the scenes. I have another property:
public UserDoc User { get; set; }
Now I am trying to fill this User property with all the user details based on the UserId property. This is the code I am trying to achieve this:
categoriesCollection.Aggregate()
.Lookup<CategoryDoc, UserDoc, CategoryDoc>(
usersCollection,
x => x.UserId,
x => x.Id,
x => x.User)
As expected, 'Lookup' is expecting an array of User documents but I have a property referencing a single user object and thus, it throws an error:
An error occurred while deserializing the User property of class TatSat.API.Documents.CategoryDoc: Expected a nested document representing the serialized form of a TatSat.API.Documents.UserDoc value, but found a value of type Array instead.
Can someone help me with this? I am new to Mongo so this is a bit of a pain for me. Kindly note that I am looking for a strongly typed solution and don't want to mess with BsonDocuments if that can be avoided.
Thanks in advance.
So I finally figured out in some ways. Basically I decided to have another class:
public class CategoryDocWithTemporaryData : CategoryDoc
{
public UserDoc[] Users { get; set; }
public static Expression<Func<CategoryDocWithTemporaryData, CategoryDoc>> ToCategoryDoc => c =>
new CategoryDoc
{
Id = c.Id,
//other properties
User = c.Users.First()
};
}
Then I use the lookup as:
categoriesCollection.Aggregate()
.Lookup<CategoryDoc, UserDoc, CategoryDocWithTemporaryData>(
usersCollection,
c => c.UserId,
c => c.Id,
c => c.Users)
.Project(CategoryDocWithTemporaryData.ToCategoryDoc)
This, however needs me to use additional projection where I have to manually select all properties which is something I wanted to avoid. But I think I can live with it until I come across a better approach.
Lookup will always return an array, even if it only has 1 element. If you need just the first (or only) document in that array, use a project/set/addFields stage with arrayElemAt.

ef core attach same entity multiple times

I'm sure this was asked before, I don't know what to search for, so it's probably duplicate.
I have code that adds new entity to database. This entity has reference to another entity(Role), and I get it via service. Service creates another instance of dbContext, so I have to attach role to the context after I fetch it. The problem is, when I try to attach two same roles, I get this exception:
'Role' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.'
How should I do it? Code below:
using (var context = new TenantContext(schemaName, connectionString))
{
ApprovalTemplates templates = new ApprovalTemplates();
ApprovalTemplate template = new ApprovalTemplate();
template.Approvers = new List<StageTemplate>();
foreach (var stage in request.Stages)
{
var temp = new StageTemplate();
temp.Order = stage.Order;
temp.Name = stage.Name;
var role = roleService.GetById(stage.RoleId, schemaName);//here I get the role
temp.AvailableActions = new List<ApprovalActionTemplate>();
foreach (var actionId in stage.Actions)
temp.AvailableActions.Add(context.ApprovalActions.First(a => a.Id == actionId));
//when I try to add already attached role, exception is thrown
context.TenantRoles.Attach(role);
temp.Role = role;
template.Approvers.Add(temp);
}
templates.PRApprovalTemplate = template;
context.ApprovalTemplates.Add(templates);
context.SaveChanges();
}
I would share potential approach for this and similar cases with Attach - the rule is very simple, you should never attach Entity with the same Id twice. Good point that there is an easy way to check if it's already attached and if it's attached, you can just use that entity, so best way is to always check local entities before attaching any Entity.
For your case in place of
var role = roleService.GetById(stage.RoleId, schemaName);//here I get the role
it may be:
var localRole = context.Set<TenantRole>().Local.FirstOrDefault(entry => entry.Id.Equals(stage.RoleId));
if (localRole == null)
{
localRole = new TenantRole
{
Id = stage.RoleId,
};
Context.TenantRoles.Attach(localRole);
}
...
temp.Role = localRole;
Because if you know RoleId, you do not need to make DB call just to attach TenantRole to the Context.
Given code works fine, but once someone have many-many places like this, it's becomes to heavy. Potential solution for this would be creating extension method for your Context:
public static class RepositoryExtensions
{
public static T LocalContextEntitiesFinder<T>(this TenantContext context, Guid id) where T : class, ISomeInterfaceThatAllYourDBModelsImplements, new()
{
var localObj = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(id));
if (localObj != null)
{
return localObj;
}
localObj = new T
{
Id = id
};
context.Set<T>().Attach(localObj);
return localObj;
}
}
So you will be able to re-write your code to something like:
...
temp.Role = context.LocalContextEntitiesFinder<TenantRole>(id: stage.RoleId);
...
To make it work, you should add interface ISomeInterfaceThatAllYourDBModelsImplements similar to this (in place of Guid you can use any other type you like):
public interface ISomeInterfaceThatAllYourDBModelsImplements
{
public Guid Id { get; set; }
}
And update TenantRole
public class TenantRole: ISomeInterfaceThatAllYourDBModelsImplements
{
[Key]
public Guid Id { get; set; }
...
I hope this may help somebody.

Entity Framework inheritance

SQL Layer:
I have a table
Entity Framwork Layer:
I have the following rule: all Offers, which have State is null, are Outstanding offers, State is true are Accepted offers, State is false are Declined offers. Also, part of fields used only for Outstanding, part - only for Accepted etc... I use Database first approach, so, I updated EF model from DB and renamed Offer entity to OfferBase and created 3 child classes:
it works fine for add/select entities/records. Right now I want to "move" offer from outstanding to accept offer, so, I need to set Status=true (from Status is null) for appropriate record. But how to do it by Entity Framework? If I try to select outstanding offer as Accept offer I get an null reference (and clearly why)
// record with ID=1 exists, but State is null, so, EF can not find this record and offer will be null after the following string
var offer = (from i in _db.OfferBases.OfType<EFModels.OfferAccepted>() where i.ID == 1 select i).FirstOrDefault();
if I try to select as OfferBase entity I get the following error:
Unable to cast object of type
'System.Data.Entity.DynamicProxies.OfferOutstanding_9DD3E4A5D716F158C6875FA0EDF5D0E52150A406416D4D641148F9AFE2B5A16A'
to type 'VTS.EFModels.OfferAccepted'.
var offerB = (from i in _db.OfferBases where i.ID == 1 select i).FirstOrDefault();
var offer = (EFModels.OfferAccepted)offerB;
ADDED NOTES ABOUT ARCHITECTURE:
I have 3 types of Offer entity. There are: AcceptOffer, DeclineOffer and OutstandingOffer.
AcceptOffer:
UserID
ContactID
Notes
FirstContactDate
LastContactDate
[... and 5-10 the unique fields...]
DeclineOffer:
UserID
ContactID
Notes
[... and 5-10 the unique fields...]
OutstandingOffer:
UserID
ContactID
FirstContactDate
LastContactDate
[... and 5-10 the unique fields...]
How to do it correctly? Of course, I can select a record, remove from DB and add new with appropriate state value, but how to do it normally?
You can't change the type of an object once it's created. Your object model seems wrong.
Either you delete the outstanding offer and create an accepted offer from it (looks like what you are currently doing) but you may lose relations as you created a new object with a new identity (you can also copy them before removing the old object). Or you want to keep the same object and change its state.
If you want to keep the same identity then preffer composition over inheritance.
Your code could look like this :
public class Offer
{
public int Id { get; set; }
public virtual OfferState State { get; set }
}
public class OfferState
{
public int OfferId { get; set; }
public string Notes { get; set; }
}
public class AcceptedOfferState : OfferState
{
public DateTimeOffset AcceptDate { get; set; }
}
public class DeclinedOfferState : OfferState
{
public DateTimeOffset DeclinedDate { get; set; }
}
If you still want to change the type of the object and keep its identity then you may use stored procedures ; as stated by Noam Ben-Ami (PM owner for EF) : Changing the type of an entity.
Rather than trying to add these custom classes to your entity framework model, just create them as normal c# classes and then use a projection to convert from the entity framework generated class to your own class e.g.
var accepteOffers= from i in _db.Offers
where i.ID == 1 && i.Status == true
select new OfferAccepted { AcceptDate = i.AcceptDate, StartTime = i.StartTime /* map all releaveant fields here */};

Entity Framework Lazy Loading Does Not Work

I am migrating my project from EF 4.0 to EF 6.0.
I am trying to get properties from navigated entity after insert new entity. That code runs without any problem in EF 4.0 but when I try to run in EF 6.0 I am gettin NullReference Exception.
User Entity:
public partial class users
{
public int RID { get; set; }
public string Name { get; set; }
public Nullable<int> LangRef { get; set; }
public virtual language language { get; set; }
}
Language Entity:
public partial class language
{
public language()
{
this.users = new HashSet<users>();
}
public int RID { get; set; }
public string Name { get; set; }
public virtual ICollection<users> users { get; set; }
}
Test Code:
test1Entities testEnt = new test1Entities();
users user = new users();
user.Name = "asd";
user.LangRef = 1;//That ID refer to English record in Language entity
testEnt.users.Add(user);
testEnt.SaveChanges();
string lang = user.language.Name;//Should return "English". However language property seem null
Model:
You say that user.language.Name should return English, you are not setting that anywhere. You have to set the language when you create a new user.
test1Entities testEnt = new test1Entities();
users user = new users();
user.Name = "asd";
user.LangRef = 1;
// grab the english language entity from the database
language englishLanguage = entities.First(o => o.Name == "English");
// set the users language to english
user.language = englishLanuage;
testEnt.users.Add(user);
testEnt.SaveChanges();
Imagine if you were inserting a user with raw SQL, you would run something like this
INSERT INTO users (name, languageId) VALUES ('Foo', 1);
Where 1 is the RID of the record in the language table which maps to the english language. The 1 is a Foreign Key in the users table which maps a user to their language. Same as with the raw SQL you must explicitly tell Entity Framework which language the user has. This is done by querying the DbContext for the language (which returns a tracked entity), then setting that entity to the navigation property.
Alternatively, if you know the primary key of the language record in advance, you can create a new language entity (with the correct RID) and attach it to the DbContext, then set it to the navigation property. This would make it so there is one less query against the database. While this would be a little more effecient, you should do whatever is easier/makes the most sense to you. Get it working, then you can optimize bottlenecks later.
language englishLanguage = new language() { RID = 1, Name = "English" };
testEnt.languages.Attach(language);
user.language = englishLanguage;
testEnt.users.Add(user);
Edit
Given your recent edits, it is now more clear what you are trying to do. I didn't realize that LangRef was your foreign key since it was a decimal and doesn't follow normal naming conventions.
The solution is described in this answer, https://stackoverflow.com/a/18251442/1160036. The problem arises from Entity Framework caching. After SaveChanges() (so that the RID is updated), you can detach and re-pull the user to refresh it.
testEnt.users.Add(user);
testEnt.SaveChanges();
((IObjectContextAdapter)testEnt).ObjectContext.Detach(user);
users user = testEnt.First(o => o.RID == user.RID);
Console.WriteLine(user.language.Name);
Opening a new DbContext works as well, but this does cause another connection to be made to the database.
As described in the above answer, testEnt.Entry<users>(user).Reload() unfortunately doesn't work.
Instead of using new users() try testEnt.users.Create(). The difference is that the object returned will be a proxy class which is needed for lazy loading of navigation properties.
As an aisde, doing the opposite and setting the language navigation property itself and not the LangRef foreign key value should actually correctly set the LangRef property when you call SaveChanges().
See DbSet.Create method on MSDN.

Inheriting from EFF db model class leads to derived class properties being written into database

Let's say I have a EFF db model like this:
public class DbEFF
{
[Key]
public long Id { get; set; }
}
Now I'm creating a class where I'm inheriting from the db class like this:
public class DbTest:DbEFF
{
public DbTest(long id)
{
Id=id;
}
public string someotherproperty1 {get;set;}
}
Now I call the following code to write into the database:
var db = new DbEFF();
db.Id = "454545";
var model = new MasterEntities();
model.Table1.Add(db);
model.SaveChanges();
The weird thing now is that I get an inner exception saying that the column someotherproperty1 does not exist. What am I missing here? Why is the properties of the derived class being exposed like this?
The exception I'm getting is:
{"Invalid column name 'someotherproperty1'."}
In entity framework code first table per Hierarchy (TPH) is the default mapping.
This means that EF will map both DbTest and DbEFF to the same table. It will also add a column called Discriminator to see store what type of object is persisted (DbTest or DbEFF) in a particular row.
From the error you get it seems that your database already exist and that it has been created before you added the 'someotherproperty'. I.e. your table doesn't have 'someotherproperty' column.
To fix this there are several options, you need to get the schema of the table to match your classes or you must choose a different mapping strategy.
The easiest fix is to simply drop your database and let EF create the right database for you.

Categories