简体   繁体   中英

How to get Column name and corresponding Database Type from DbContext in Entity Framework Core

Suppose I have this table:

在此处输入图像描述

How can I get the column name and database datatype from DbContext in Entity Framework Core?

Tips

  1. The column with name clg# converted to clg1 by EF Core Scaffold tool so I need real column name not current EF name

  2. I need database type, not clrType, of course the must be cross platform. Maybe I will change the database so the method must work too.

Desired result:

    <D.clg#, int>
    <D.clgname, nvarchar(50)>
    <D.city, nvarchar(50)>
    <D.pname, nvarchar(50)>

Can anyone provide a solution?

Update (EF Core 3.x): Starting with EF Core 3.0, the metadata API has changed again - Relational() extensions have been removed, and properties have been replaced with Get and Set extension methods, so now the code looks like this:

var entityType = dbContext.Model.FindEntityType(clrEntityType);

// Table info 
var tableName = entityType.GetTableName();
var tableSchema = entityType.GetSchema();

// Column info 
foreach (var property in entityType.GetProperties())
{
    var columnName = property.GetColumnName();
    var columnType = property.GetColumnType();
};

Update (EF Core 2.x): Starting with EF Core 2.0, the things have changed, so the original answer does not apply anymore. Now EF Core builds separate model for each database type, so the code is much simpler and uses directly the Relational() extensions:

var entityType = dbContext.Model.FindEntityType(clrEntityType);

// Table info 
var tableName = entityType.Relational().TableName;
var tableSchema = entityType.Relational().Schema;

// Column info 
foreach (var property in entityType.GetProperties())
{
    var columnName = property.Relational().ColumnName;
    var columnType = property.Relational().ColumnType;
};

Original answer (EF Core 1.x):

Getting the access to the associated metadata is much easier in EF Core compared to EF - you start from DbContext.Model property to get IModel , use GetEntityTypes or FindEntityType to get IEntityType , then GetProperties or FindProperty to get IProperty etc.

However the problem is that EF Core allows you to use different setting fro different target database. In order to get the attributes corresponding to the current database used by the context, you need to get access to the IRelationalDatabaseProviderServices and use AnnotationProvider and TypeMapper properties to get the information needed.

Here is an example:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Storage;

public class DbColumnInfo
{
    public string Name;
    public string Type;
}

public static class RelationalDbHelpers
{
    public static IEnumerable<DbColumnInfo> GetDbColums(this DbContext dbContext, Type clrEntityType)
    {
        var dbServices = dbContext.GetService<IDbContextServices>();
        var relationalDbServices = dbServices.DatabaseProviderServices as IRelationalDatabaseProviderServices;
        var annotationProvider = relationalDbServices.AnnotationProvider;
        var typeMapper = relationalDbServices.TypeMapper;

        var entityType = dbContext.Model.FindEntityType(clrEntityType);

        // Not needed here, just an example 
        var tableMap = annotationProvider.For(entityType);
        var tableName = tableMap.TableName;
        var tableSchema = tableMap.Schema;

        return from property in entityType.GetProperties()
               let columnMap = annotationProvider.For(property)
               let columnTypeMap = typeMapper.FindMapping(property)
               select new DbColumnInfo
               {
                   Name = columnMap.ColumnName,
                   Type = columnTypeMap.StoreType
               };
    }
}

For EF Core 5, you need to use the overload that accepts a StoreObjectIdentifier: GetColumnName(IProperty, StoreObjectIdentifier).

Update for EF 5:

var schema = entityType.GetSchema();
var tableName = entityType.GetTableName();
var storeObjectIdentifier = StoreObjectIdentifier.Table(tableName, schema);
var columnName = entityType.FindProperty(propertyName).GetColumnName(storeObjectIdentifier);

For those who arrived here, but are not using .NET CORE, like me. try this:

public partial class MyDbContext : System.Data.Entity.DbContext
{
    public string GetTableName(Type entityType)
    {
        var sql = Set(entityType).ToString();
        var regex = new Regex(@"FROM \[dbo\]\.\[(?<table>.*)\] AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    public string[] GetColumnName(Type entityType)
    {
        var strs = new List<string>();
        var sql = Set(entityType).ToString();
        var regex = new Regex(@"\[Extent1\]\.\[(?<columnName>.*)\] AS");
        var matches = regex.Matches(sql);
        foreach (Match item in matches)
        {
            var name = item.Groups["columnName"].Value;
            strs.Add(name);
        }
        return strs.ToArray();
    }
}

maybe redundante, but it saves time.

antonio

Based on EFC3.1 answer I have created this helper to store all table names and column names into an dictionaries of a singleton populated on first use so the code doesn't have to traverse everything again and again. We use it for NPGSQL bulk copy operations and it seems to work properly. This version does not filter out any properties from entity classes so be careful about ordering of fields when doing column name lists/strings. But then again as I understand it, you will get only properties that are mapped in context so everything might be ok.

The helper

public class ContextHelper
  {
    private readonly ILogger<ContextHelper> logger;
    private readonly ApplicationDbContext context;

    private static Dictionary<Type, string> tableNames = new Dictionary<Type, string>(30);

    private Dictionary<Type, Dictionary<string, string>> columnNames = new Dictionary<Type, Dictionary<string, string>>(30);

    public ContextHelper(ILogger<ContextHelper> logger, ApplicationDbContext context)
    {
      this.logger = logger;
      this.context = context;

      PopulateTableNames();
      PopulateColumnNames();
    }

    private void PopulateTableNames()
    {
      logger.LogInformation("Populating table names in context helper");

      foreach (var entityType in context.Model.GetEntityTypes())
      {
        tableNames.Add(entityType.ClrType, entityType.GetTableName());
      }
    }

    private void PopulateColumnNames()
    {
      logger.LogInformation("Populating column names in context helper");

      foreach (var entityType in context.Model.GetEntityTypes())
      {
        var clrType = entityType.ClrType;

        if (!columnNames.ContainsKey(clrType))
        {
          columnNames.Add(clrType, new Dictionary<string, string>(30));
        }

        foreach (var property in entityType.GetProperties())
        {
          columnNames[clrType].Add(property.Name, property.GetColumnName());
        }
      }
    }

    public string GetTableName<T>()
    {
      return context.Model.FindEntityType(typeof(T)).GetTableName();
    }

    public string GetColumnName<T>(string propertyName)
    {
      return columnNames[typeof(T)][propertyName];
    }

    public List<string> GetColumnNames<T>()
    {
      return columnNames[typeof(T)].Select(x => x.Value).ToList();
    }
  }

Startup registration

services.AddSingleton<ContextHelper>();

Usage, something along these lines

var columnNames = contextHelper.GetColumnNames<OvenEventLog>().Where(x=>x != contextHelper.GetColumnName<OvenEventLog>(nameof(OvenEventLog.IdLog)));
var separatedCN = string.Join(", ", columnNames);

using (var writer = conn.BeginBinaryImport(
    $"COPY {contextHelper.GetTableName<OvenEventLog>()} ({separatedCN}) FROM STDIN (FORMAT BINARY)")

I created and used this function to do the exact thing on EF Core 5.0

public static List<ColumnInfo> GetAllColumns(this DbContext db, Type TableName)
{
    var entityType = db.Model.FindEntityType(TableName);
    var properties = entityType.GetProperties();
    List<ColumnInfo> columns = new List<ColumnInfo>(properties.Count());
    foreach (var property in properties)
        columns.Add(new ColumnInfo(property.GetColumnName(StoreObjectIdentifier.SqlQuery(entityType)), property.GetColumnType()));

    return columns;
}

public class ColumnInfo
{
    public string Name;
    public string Type;

    public ColumnInfo(string Name, string Type)
    {
        this.Name = Name;
        this.Type = Type;
    }
}

you can also replace string Type to SqlDbType Type and so, the foreach line would be like this:

columns.Add(new ColumnInfo(
    property.GetColumnName(StoreObjectIdentifier.SqlQuery(entityType)), 
    Enum.Parse<SqlDbType>( property.GetColumnType().Slice(0,"(",true), true)));

where Slice is my function, there's a necessary piece of code:

public static string Slice(this string s, int Start, string EndsWith, bool AlwaysReturnString = false)
{
    var end =s.LastIndexOf(EndsWith);
    if (end < 0) return AlwaysReturnString? s : null;

    if (Start > end) throw new ArgumentException($"start ({Start}) is be bigger than end ({end})");

    return s.Slice(Start, end);

}

public static int IndexOfEnd(this string s, string s2)
{
    if (s == null)
        if (s2.Length == 0)
            return 0;
    int i = s.IndexOf(s2);
    return i == -1 ? -1 : i + s2.Length;
}

public static string Slice(this string s, int Start = 0, int End = Int32.MaxValue)
{
    if (Start < 0) throw new ArgumentOutOfRangeException($"Start is {Start}");
    if (Start > End) throw new ArgumentException($"start ({Start}) is be bigger than end ({End})");
    if (End > s.Length) End = s.Length;
    return s.Substring(Start, End - Start);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM