0

In an old WPF project I have a class with Properties like this:

    private string _name = "";
    public string Name
    {
        get { return _name; }
        set
        {
            string cleanName = clsStringManip.CleanText(value, true);
            if (cleanName != _name)
            {
                _name = cleanName;
            }
        }
    }

Where every time the name changes, I ensure that the value is "cleaned". Putting it in the property ensures I never forget to clean the string before setting the property on the object.

Now I am recreating this system using MVC5 and EntityFramework6.1 using DatabaseFirst.

So all the properties are autogenerated by EF. How then can I add the equivalent CleanText function to my properties without editing the autogen code? - as I'll lose these changes next time I change my database and resync.

All I can find via Google is a way add data annotations via MetadataType and partial classes but this doesn't answer my question.

I tried to add the above code into a partial class but get the error:

The type XXX already contains a definition for Name

The only way I can think is to create a bunch of SetProperty() functions but this is dirty and you can never ensure other developers (or myself) will remember to use them.

Percy
  • 2,855
  • 2
  • 33
  • 56
  • What version of the EF? Code-first, model-first, or database-first? – Moby Disk Feb 18 '15 at 19:29
  • Hi EF 6.1, MVC 5, Database first – Percy Feb 18 '15 at 19:38
  • 1
    You may consider going code-first. I don't think it's a good idea to modify the t4 code generation template for such a profound change. – Gert Arnold Feb 18 '15 at 19:38
  • I was considering code first but my database is a substantial one and as i'm new to MVC and EF I don't really have the timescale for the learning curve unfortunately. How do you mean Profound? - How would you achieve the same type of thing? – Percy Feb 18 '15 at 19:40
  • It's quite substantial to change the default generation auto properties into these guarded setters. You'd have to modify the t4 template at a number of places (e.g. also generate member variables) and maybe only for selected properties. With CF this is much easier. You can create a CF model from the database to get started. – Gert Arnold Feb 18 '15 at 19:45
  • You can generate POCO classes for an existing database if you want to go code-first. – Rob Tillie Feb 18 '15 at 19:46
  • @gertArnold thanks for the info - yes I don't fancy going down the t4 template alteration route - sounds like I'd be asking for trouble! – Percy Feb 18 '15 at 20:13

3 Answers3

2

Disclaimer: I haven't used EF 6 yet.

Let me answer this in two parts. First, I will tell you how to do this. Then I will tell you why I don't think you should do this. :-)

HOW:

As you discovered, you cannot create another Name property. You need to modify the way the EF generates the code, so that it gives you a place to insert your new code. Depending on how you are using the EF, it often generates Validate() method calls or OnPropertyChanged() calls. You may be able to do what you want inside of those methods.

If you can't do this in Validate() or OnPropertyChanged(), you could change the T4 template to generate something like this:

private string _name = "";
public string Name
{
    get { return _name; }
    set
    {
        string cleanName = value;
        Cleanup_Name(ref cleanName);
        if (cleanName != _name)
        {
            _name = cleanName;
        }
    }
}

private partial void Cleanup_Name(ref string);

This gives you a partial method that you can then implement as you see fit. So for any property you want to customize, you can now add another file to your project that does this:

public partial class MyEntity {
   void Cleanup_Name(ref string name)
   {
      // Put your logic in here to fixup the name
   }
}

If you do not write the above code block, then the partial method is simply a no-op. (Partial methods must return void, hence the use of a ref parameter).

WHY NOT?

The advantage of this method is that it is totally transparent to the developer. The property is just magically changed. But there are several disadvantages:

Some controls expect that if they call name = "123" that if they get the name back, it is "123" and will fail if this happens. Values are changing but no PropertyChanged event fired. If you do fire the PropertyChanged, then they sometimes change the value back. This can cause infinite loops.

There is no feedback to the user. They typed in one thing, and it looked right, but now it says something different. Some controls might show the change and others won't.

There is no feedback to the developer. The watch window will seemingly change values. And it is not obvious where to see the validation rules.

The entity-framework itself uses these methods when it loads data from the database. So if the database already contains values that don't match the cleanup rules, it will clean them when loading from the database. This can make LINQ queries misbehave depending on what logic is run on the SQL server and what logic is run in the C# code. The SQL code will see one value, the C# will see another.

You might also want to look into what the Entity-Framework's change tracking does in this case. If a property set does a cleanup while loading values from the database, does it consider that a change to the entity? Will a .Save() call write it back to the database? Could this cause code that never intended to change the database to suddenly do so?

ALTERNATIVE

Instead of doing this, I suggest creating a Validate() method that looks at each property and returns errors indicating what is wrong. You could also even create a Cleanup() method that fixes the things that are wrong. This means the cleanups are no longer transparent, so the developer must call them explicitly. But that is a good thing: the code isn't changing values without them realizing it. The person writing the business logic or the UI knows at what point the values will change, and can get a list of why.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
Moby Disk
  • 3,761
  • 1
  • 19
  • 38
  • awesome answer @Moby Thanks. All your points are totally valid. When I saw your template change, I though "Brilliant", but after reading your "Why Not" part I can see that I should be calling the cleanup code manually for all the reasons you state. Yes a long the line I/we/everyone will forget to call clean up at somepoint or another but atleast it will be clear and we can fix the dirty string and ensure the clean call is added where required. – Percy Feb 18 '15 at 20:40
1

The only way you can achieve this is by creating a new property you actually use in your application. Perhaps you can hide the original property in the designer. The actual property you use could look like this:

public string ExternalName
{
    get { return Name; }
    set
    {
        string cleanName = clsStringManip.CleanText(value, true);
        if (cleanName != Name)
        {
            Name = cleanName;
        }
    }
}

As an alternative, you can use POCO classes:

Community
  • 1
  • 1
Rob Tillie
  • 1,137
  • 8
  • 30
  • Yes this was the idea I had but using Properties rather than the Setter Menthods. – Percy Feb 18 '15 at 20:14
  • Is the codefirst but with existing database basically just creating the code to match the existing database? I suppose I'd have to be careful to name everything exactly as the DB or I'd risk a mismatch with the database - I think this is bound to happen somewhere down the line though. – Percy Feb 18 '15 at 20:16
  • 1
    I added a different guide. It's an excellent step-by-step tutorial for doing this. Visual Studio will generate the entire POCO model for you from the existing database and do all the reverse engineering for you – Rob Tillie Feb 18 '15 at 20:26
  • thanks for the guide - it looks fairly straight forward and i'm considering it - it will get me to the code first situation that I wanted to be in. Being new to MVC EF and Codefirst. Once I've reverse engineered my DB to the POCO model, should I then "forget" about my database and do everything in the code? – Percy Feb 19 '15 at 07:21
  • 1
    @Rick yes, the reverse engineering is meant as a one-time thing at the start. Then you can continue using code first and something like EF Migrations to update the database to newer versions. I have used all three approaches, code-first is a lot easier once you get used to it. You could take a couple of hours to give it a testdrive, and see how it works for you. – Rob Tillie Feb 20 '15 at 07:49
  • when you tried each approach did you notice any kind of performance issues with any? I guess if your testing was only with small data then if there is any difference between approaches it would be unnoticeable. I've seen some blogs that suggest code first is not as efficient as database first...though no one has shown me any evidence to prove this. – Percy Feb 20 '15 at 15:35
1
  1. Add partial to the generated class.
  2. Change the scope of Name in the generated class from public to internal.
  3. Add the following in the same assembly:

 

public partial class classname
{
    [NotMapped]
    public string CleanName
    {
        get { return Name; }
        set
        {
            var cleanName = clsStringManip.CleanText(value, true);
            if (cleanName != Name)
                Name = cleanName;
        }
    }
}
  1. Caveat: you'd have to remember to do steps 1-2 every time you regenerated your POCOs ... I'd seriously consider Code First to Existing Database.

EDIT

Optionally:

  1. Rename Name as InternalName in the generated classname; decorate it with [Column("Name")].
  2. Rename CleanName as Name in the partial class under your control.
  3. Caveat in 4 becomes "remember to do steps 1, 2, and 5 every time you regenerate POCOs".

This approach has the added benefit of not having to modify any of your client code (i.e., use of Name remains Name). And I'd still strongly consider Code First to Existing Database.

John Castleman
  • 1,552
  • 11
  • 12
  • Nice solution to the problem. I guess remembering to do that would be easy as I'd get compilation errors everytime I did regenerate. The bit I would probably forget is calling CleanName rather than Name. – Percy Feb 20 '15 at 15:32
  • yeah, let me know here in the comments if you wind up using any of it. – John Castleman Feb 21 '15 at 19:54