3

We just Scaffolded our database, and created Models from Database tables in Entity Framework.

Additionally, we are creating files with Ids which Map to the Primary Key. The purpose of this is to map to our Generic Repository Interface which utilizes Id.

How do I go through all my 200 + models, and create a file similar to file 2 below. I've seen it conducted at previous workplaces. Trying to research. Is there a Visual Studio or Entity framework feature which loops through all models automatically? Currently I am going through each model, and creating the id manually as seen in Generic Id File 2. Willing to implement T4 which implements code generation however other solutions are good.

Scaffolded Files 1:

namespace Datatest
{
    public partial class Property
    {
        public int Property { get; set; }
        public int DocumentId { get; set; }
        public string Address { get; set; }
    }
}

Generic ID File 2:

public partial class Property: IEntity
{
    [NotMapped]
    public int Id { get => PropertyId; set => PropertyId = value; }
}

Sample Generic Base Repository for all tables:

    public T Get(int id)
    {
        return Table.Find(id);
    }
    public async Task<T> GetAsync(int id)
    {
        return await Table.FindAsync(id);
    }
    public T Single(Expression<Func<T, bool>> predicate)
    {
        return All.Single(predicate);
    }
    public async Task<T> SingleAsync(Expression<Func<T, bool>> predicate)
    {
        return await All.SingleAsync(predicate);
    }
    public T FirstOrDefault(int id)
    {
        return All.FirstOrDefault(CreateEqualityExpressionForId(id));
    }

Maybe this resource helps? Trying to make it loop through all my model files now How to create multiple output files from a single T4 template using Tangible Editor?

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".txt" #>
<#
for (Int32 i = 0; i < 10; ++i) {
#>
Content <#= i #>
<#
  // End of file.
  SaveOutput("Content" + i.ToString() + ".cs");
}
#>
<#+
private void SaveOutput(string outputFileName) {
  string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
  string outputFilePath = Path.Combine(templateDirectory, outputFileName);
  File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); 
  this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
}
#>
jerrythomas38
  • 759
  • 2
  • 16
  • 41
  • Read up on this https://medium.com/falafel-software/implement-step-by-step-generic-repository-pattern-in-c-3422b6da43fd – Zakk Diaz Jul 29 '19 at 22:40
  • Or this https://deviq.com/repository-pattern/ – Zakk Diaz Jul 29 '19 at 22:42
  • 1
    And finally, to do it for all 200 classes, generate the code your self either from a sql query or from t4 template https://learn.microsoft.com/en-us/visualstudio/modeling/walkthrough-generating-code-by-using-text-templates?view=vs-2019 – Zakk Diaz Jul 29 '19 at 22:43
  • 2
    I would suggest you reconsidering the whole design. Mapping via interface and not mapped property simply won't work for LINQ to Entities queries. Given the fact that EF Core metadata system provides that information (how do you think `Find` works), keep the entity classes *as is* (no interface, no partial classes) and implement `CreateEqualityExpressionForId(id)` using EF Core metadata - for example, see my answer to [Generic Repository in C# Using Entity Framework](https://stackoverflow.com/questions/55867725/generic-repository-in-c-sharp-using-entity-framework/55880724#55880724). – Ivan Stoev Jul 30 '19 at 00:36
  • It's essentially the same. Just instead of `Contains` you would use `==` operator, e.g. `x => EF.Property(x, idName) == id` – Ivan Stoev Jul 30 '19 at 07:18
  • Since this is not answering your concrete question, I'll stay with comments only. And for those who are asking you to use interface (and I guess `where T : class, IEntity` constraint), let them try `x => x.Id == id`. The result will be either client evaluation (reading the whole table in memory and then applying the filter) or runtime exception. – Ivan Stoev Jul 30 '19 at 07:58

2 Answers2

4

I agree with @Ivan, I wouldn't recommend you this way, but you answered that you need to, so here we go.

You're using EFCore right ? Luckily, EFCore is open-source, so we can dig into the source code and build custom EFCore versions.

A few months ago I had also a specific need with EF Context scaffolding, we also have over 200 tables and needed to put mappings for each table in a separate class, because EF Core, defaults to put all the mapping stuff in the DbContext file and this generated a 10k+ lines of code long DbContext class for us .

EntityTypes generation is handled here. Interesting line for you is #109 :

_sb.AppendLine($"public partial class {entityType.Name}");

Here you could just add your Interface this way :

_sb.AppendLine($"public partial class {entityType.Name} : IEntity");

Then we have to implement your interface, on line #113 we have the following code :

using (_sb.Indent())
{
   GenerateConstructor(entityType);
   GenerateProperties(entityType);
   GenerateNavigationProperties(entityType);
}

Just before GenerateProperties(entityType); you can add the following lines to implement interface specification :

_sb.AppendLine("[NotMapped]");
_sb.AppendLine("public int Id { get => PropertyId; set => PropertyId = value; }");
_sb.AppendLine("");

If your really need/want partial classes you could simply write some code to generate another file in the WriteCode method which is called once per table and has all the info needed for this (type name, etc.)

Here is the code for building the project with your custom implementation. After scaffolding your project you can just return to a official EFCore build.

Selmir
  • 1,136
  • 1
  • 11
  • 21
1

With T4 Research, answer below. People are free to edit/optimize.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>

<#
    DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(this.Host.TemplateFile) + @"\Scaffold");
    FileInfo[] Files = d.GetFiles("*.cs");
    string str = "";

    foreach(FileInfo file in Files )
    {
        var modelName = Path.GetFileNameWithoutExtension(file.Name);
#>
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace Data.Entities
{
    public partial class <#=modelName #> : IEntity, IAuditedEntity
    {
        [NotMapped]
        public int Id { get => <#=modelName #>Id; set => <#=modelName #>Id = value; }
    }
}

<#
  // End of file.
  SaveOutput(file.Name.ToString());
}
#>
<#+
    private void SaveOutput(string outputFileName) {
      string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
      string outputFilePath = Path.Combine(templateDirectory, outputFileName);
      File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); 
      this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
    }
#>

Update:

For some reason, code is also making a copy of the Code Generation T4 File. Trying to fix it now, someone can write new answer, and I will accept it, Thanks.

jerrythomas38
  • 759
  • 2
  • 16
  • 41