msawczyn/EFDesigner

Issues with Designer, VS2019, .NET Core 3.0

alprinias opened this issue · 16 comments

Hello and thank you for a great tool.

I just started using it a couple of days ago for a new project with VS2019 Community and .NET Core 3.0. I followed the docs and created a couple of entities, all fine. I came across a problem when I tried to create an overall "BaseEntity". My idea is to place a few properties that will be shared amongst all other entities, things like the primary key Id (Int32) and CreatedBy, ModifiedBy (String) and CreatedOn, ModifiedOn (DateTime). I noticed an "unstable" behaviour when I added the last four by "Add properties Via Code" functionality, where I got an "object reference ..." error. I added by code only one of the String properties and it worked, so I thought that it was the datetime properties, and then tried to add them manually, i.e. create a new property, change its name from Property1 to CreatedOn, make it Required, but when I tried to change its Type from String to DateTime I got an error saying "cannot set property value on a deleted element". After a couple of tries, deleting the property and recreating it,I finally managed to do what I intended.

And then, I tried to connect with an inheritance link my "Country" entity (with just two properties, Id and Name) with the BaseEntity. Then Visual Studio itself crashes, and when it restarts it shows the yellow header with "A previous session ended unexpectedly. Disabling the extension Entity Framework Visual Editor 1.3.0.6 might help." Edit: after I removed the property Id from the child entity, it worked alright.

The other question is whether the BaseEntity will be created as a table in the DB or it will just have its properties added to all the inheriting tables.

The designer surface is missing the properties described in the documentation "Inheritance Strategy", "Lazy loading Enabled" and "Proxy Generation Enabled" from teh Code Generation group. Also "Automatic Migrations Enabled" and "Database Initializer Type" are missing from the Databse group. Should they be there or is the documentation outdated?

All the best,
Alex

Hi Alex,

You said you're using EFCore. That technology is different than the older EF6, and only handles one inheritance strategy (table-per-hierarchy), so you won't see that option. Automatic migrations, proxy generation and database initializers aren't supported in EFCore (yet?) either, so those options are disabled as well. The designer needs to be updated to handle lazy loading, introduced in EFCore 2.2, as the EFCore support is currently at the 2.1 level.

Since EFCore only uses TPH inheritance, you'll see a table structure I don't think you're anticipating when you use inheritance. Running the 'add-migration' procedure will create a base class table with all properties from inherited classes. If you use a common base class for all your entities, you'll end up with only one table :-) ! Not the designer's doing ... that's Entity Framework. I'm pretty confident that will change as it evolves, but don't know what their timeline is on that.

I'll look into the issues you've mentioned and see what's happening there. Thanks for the report!

Hi Michael, tnanks for your prompt and kind reply.

From what you're saying I will skip the inheritance dreams for the moment. I'm thinking about making all the classes implement an interface with the desired Created/Modified properties, so that they can all be cast to that interface.

Thank you again,
Alex

What you're wanting to do is a solved problem, and there are a lot of good solutions out in open source land that can make your life simpler. Using the designer, as well, gives you the means of modifying the generated code to add properties automatically without modifying the classes, via edits to the T4.

Search around for 'audit' and entity framework. I'm sure you'll find some things that can help.

Well, I had a look in your code and added this block in the GenerateEFCore function in the EFCoreDesigner.ttinclude file and it works! But please let me know if I'm doing something that could get me in trouble...

  //add auditing
  using (Transaction tx = modelClass.Store.TransactionManager.BeginTransaction("AddProperties"))
  {
		ModelAttribute createdByAttribute = new ModelAttribute(modelClass.Partition) { Name = "CreatedBy", Type = "String", Required = true };
		modelClass.Attributes.Add(createdByAttribute);
		ModelAttribute createdOnAttribute = new ModelAttribute(modelClass.Partition) { Name = "CreatedOn", Type = "DateTime", Required = true };
		modelClass.Attributes.Add(createdOnAttribute);

		ModelAttribute modifiedByAttribute = new ModelAttribute(modelClass.Partition) { Name = "ModifiedBy", Type = "String", Required = true };
		modelClass.Attributes.Add(modifiedByAttribute);
		ModelAttribute modifiedOnAttribute = new ModelAttribute(modelClass.Partition) { Name = "ModifiedOn", Type = "DateTime", Required = true };
		modelClass.Attributes.Add(modifiedOnAttribute);

		tx.Commit();
  }

An interesting approach but, as I'm sure you've discovered by now, not the most correct way of doing this.

The first time you generate code, you'll see the code you were expecting, and the attributes will be added to the model. Modifying the model from the T4 isn't really something you want to do; the T4 is the consumer of the data in the model and, if the model could be made readonly when the T4 runs we'd certainly do that to enforce the dependency direction.

Should the model somehow be saved at this point, you'll have the issue of the properties being added again the next time you generate code. Since duplicate names aren't allowed, you'll get unique names based on the name you asked for, and you'll have to delete them.

To take this approach, it would be more like:

   using (Transaction tx = modelClass.Store.TransactionManager.BeginTransaction("AddProperties"))
   {
      if (!modelClass.Attributes.Any(x => x.Name == "CreatedBy"))
      {
         ModelAttribute createdByAttribute = new ModelAttribute(modelClass.Partition) { Name = "CreatedBy", Type = "String", Required = true };
         modelClass.Attributes.Add(createdByAttribute);
      }
      if (!modelClass.Attributes.Any(x => x.Name == "CreatedOn"))
      {
         ModelAttribute createdOnAttribute = new ModelAttribute(modelClass.Partition) { Name = "CreatedOn", Type = "DateTime", Required = true };
         modelClass.Attributes.Add(createdOnAttribute);
      }

      if (!modelClass.Attributes.Any(x => x.Name == "ModifiedBy"))
      {
         ModelAttribute modifiedByAttribute = new ModelAttribute(modelClass.Partition) { Name = "ModifiedBy", Type = "String", Required = true };
         modelClass.Attributes.Add(modifiedByAttribute);
      }
      if (!modelClass.Attributes.Any(x => x.Name == "ModifiedOn"))
      {
         ModelAttribute modifiedOnAttribute = new ModelAttribute(modelClass.Partition) { Name = "ModifiedOn", Type = "DateTime", Required = true };
         modelClass.Attributes.Add(modifiedOnAttribute);
      }

      tx.Commit();
   }

There are a couple of other ways you could do this. You could just output the code for these properties without adding them to the model, in the WritePersistentProperties method in EFDesigner.ttinclude.

If you don't need the properties in your classes, you could just add the properties as shadow properties so they show up in the database but not in the code. No modification of the T4 is needed for that; instead, you would use the technique found in the EF Core docs and implement that in the OnModelCreatedImpl partial in your context class.

If you do decide to modify the .ttinclude (that's why they're there, after all :-) ) remember that you can add the one you want to override to your project instead of globally modifying it for your Visual Studio installation. Info on that is in the designer docs.

Regarding the errors you've found, can you give 1.3.0.7 a try? I can't seem to replicate what you're seeing, so it might be fixed already.

Thanks again for the report.

Hi Michael,
I have still a lot to learn...
Si, I added the properties in the WritePersistentProperties method. I have already copied the ttinclude files from the strange Program Files(x86)....\ folder to my project folder and modify those and not the global ones.

FYI, I installed the 1.3.0.7. Same behaviour. I'm attaching screenshots from steps with, hopefully, understandable names.
1AddedEntity
2RenamedToBaseEntityAndAddingPropertiesViaCode
3PressOK_Error_IdDisappears

Hi Michael,
Now, with 1.3.0.7 it has started giving me the error
Invoke validation method 'StringsShouldHaveLength' failed due to a null reference being accessed.

I have added a MaxLength to all my string properties but the error insists, it shows up 10 times in the Error List window.

I can send you the solution to check for yourself.

Best,
Alex

This morning I started VS again, after having shut down, and the validation errors had gone. However, as soon as I tried to add properties via code the "object reference..." error showed up. After deleting the entity from the designer and trying to validate, I get two of the errors I mentioned yesterday.
"Invoke validation method 'StringsShouldHaveLength' failed due to a null reference being accessed."
(yesterday I had 10 of those).
Really don't know what is happening. Please tell me where I can send you the solution.

Alex, please add it to an accessible location (e.g., Google drive, etc.) and send me the url at michaelsawczyn[at]gmail.com

Thanks!

That was a huge help. A couple of things:

  • Changes in 1.3.0.8 that were done for other reasons seem to have fixed the issue. I've confirmed that in 1.3.0.7 the issue exists, but the code base I'm currently using (1.3.0.8) can't reproduce the problem.

  • There is a problem with the attribute parser when you give it initial values that are enumerations. It doesn't understand them so drops the attribute like it's supposed to. That's fixed now; it can handle simple enumeration initialization. Can't do the complex stuff ( Foo.A | Foo.B ), but that's ok ... that's too much of an edge case to spend sufficient time on to get perfect.

Thanks for sending that sample file - it did the trick.

Glad you could reproduce and it helped!
So, whan am I to expect 1.3.0.8 release? Or should I go back to an earlier version? Or just forget the Enum and keep using 1.3.0.7?

Coming soon...

Continuing with 1.3.0.7 for the moment, have removed the enumeration and my model is now slightly more complicated. I saw another symptom, which I don't know if it has to do with the problematic 1.3.0.7 version or it is a problem with my model. I had two Owned Entities and with those in the model I could not run Add-Migration, I was getting a NullException. As soon as I removed them, it all worked fine.
Edit: you can ignore this. It was coming from the fact that I was linking an Owned Entity with more than one owning entities (e.g. one ApplicationUser owned Entity with a Patient and a Doctor owning entities), which I found out it cann't be done. And actually, if it can't be done and we have to repeat the owned entities (PatientApplicationUser and DoctorApplicationUser), I don't see why not just store them in the owning tables... go figure out .. Microsoft..

Fix available for review in v1.3.0.8-pre