From 54bb977e5f1719ae1c581982daa6aa9259bd3f27 Mon Sep 17 00:00:00 2001 From: Majid Date: Sat, 28 Sep 2019 14:06:52 +0330 Subject: [PATCH] improvements --- DDDCommon.csproj | 9 +- Domain/Types/ValueObject.cs | 2 +- .../NHibernate/PostgreSqlNHibernateHelper.cs | 6 +- Utils/ErrorJsonConverter.cs | 5 +- Utils/Errors.cs | 26 +- Utils/OperationResult.cs | 15 +- Utils/SerilogPostgreSqlSink/ColumnOptions.cs | 27 ++ .../SerilogPostgreSqlSink/ColumnWriterBase.cs | 248 ++++++++++++++++++ ...LoggerConfigurationPostgreSqlExtensions.cs | 69 +++++ Utils/SerilogPostgreSqlSink/PostgreSqlSink.cs | 225 ++++++++++++++++ Utils/SerilogPostgreSqlSink/TableCreator.cs | 101 +++++++ 11 files changed, 713 insertions(+), 20 deletions(-) create mode 100644 Utils/SerilogPostgreSqlSink/ColumnOptions.cs create mode 100644 Utils/SerilogPostgreSqlSink/ColumnWriterBase.cs create mode 100644 Utils/SerilogPostgreSqlSink/LoggerConfigurationPostgreSqlExtensions.cs create mode 100644 Utils/SerilogPostgreSqlSink/PostgreSqlSink.cs create mode 100644 Utils/SerilogPostgreSqlSink/TableCreator.cs diff --git a/DDDCommon.csproj b/DDDCommon.csproj index 1600054..3ac1488 100644 --- a/DDDCommon.csproj +++ b/DDDCommon.csproj @@ -6,7 +6,7 @@ Meteor true latest - 1.0.8 + 1.2.0 @@ -15,6 +15,7 @@ + @@ -24,6 +25,12 @@ + + + + + + diff --git a/Domain/Types/ValueObject.cs b/Domain/Types/ValueObject.cs index 448d51b..b8c1a19 100644 --- a/Domain/Types/ValueObject.cs +++ b/Domain/Types/ValueObject.cs @@ -21,7 +21,7 @@ public abstract class ValueObject : IEquatable return !(obj1 == obj2); } - public bool Equals(ValueObject obj) + public virtual bool Equals(ValueObject obj) { return Equals(obj as object); } diff --git a/Infrastructure/Types/NHibernate/PostgreSqlNHibernateHelper.cs b/Infrastructure/Types/NHibernate/PostgreSqlNHibernateHelper.cs index 6b3cf08..dfbe53f 100644 --- a/Infrastructure/Types/NHibernate/PostgreSqlNHibernateHelper.cs +++ b/Infrastructure/Types/NHibernate/PostgreSqlNHibernateHelper.cs @@ -5,6 +5,7 @@ using FluentNHibernate.Conventions.Helpers; using NHibernate; using NHibernate.Dialect; +using NHibernate.Logging.Serilog; using NHibernate.Spatial.Mapping; using NHibernate.Spatial.Metadata; using NHibernate.Tool.hbm2ddl; @@ -16,6 +17,8 @@ public static class PostgreSqlSessionFactoryHelper { public static ISessionFactory Create(DbConfigurations dbConfigurations) { + NHibernateLogger.SetLoggersFactory(new SerilogLoggerFactory()); + if (dbConfigurations.UseNodaTime) NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); var dbConfigurations1 = dbConfigurations; @@ -23,8 +26,7 @@ public static ISessionFactory Create(DbConfigurations dbConfigurations) .Provider() .Dialect() .Driver() - .ConnectionString(dbConfigurations1.ConnectionString) - .ShowSql(); + .ConnectionString(dbConfigurations1.ConnectionString); if (dbConfigurations.UseNetTopologySuite) { NpgsqlConnection.GlobalTypeMapper.UseRawPostgis(); diff --git a/Utils/ErrorJsonConverter.cs b/Utils/ErrorJsonConverter.cs index 893b609..aee2969 100644 --- a/Utils/ErrorJsonConverter.cs +++ b/Utils/ErrorJsonConverter.cs @@ -25,9 +25,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s var o = new JObject { { "code", error.Code }, - { "text", error.Text }, - { "details", error.Details } + { "message", error.Message } }; + if (error.Details != null) + o.Add("details", JToken.FromObject(error.Details)); o.WriteTo(writer); } } diff --git a/Utils/Errors.cs b/Utils/Errors.cs index 2007e42..999a825 100644 --- a/Utils/Errors.cs +++ b/Utils/Errors.cs @@ -6,17 +6,19 @@ namespace DDDCommon.Utils { public static class Errors { - public static Error InternalError(string details = null) => - new Error { Code = 1, Text = "internal_error", Details = details }; - public static Error NotFound(string details = null) => - new Error { Code = 2, Text = "not_found", Details = details }; - public static Error AlreadyDone(string details = null) => - new Error { Code = 3, Text = "already_done", Details = details }; - public static Error InvalidOperation(string details = null) => - new Error { Code = 4, Text = "invalid_operation", Details = details }; - public static Error DatabaseError(string details = null) => - new Error { Code = 5, Text = "database_error", Details = details }; - public static Error DuplicateKey(string details = null) => - new Error { Code = 6, Text = "duplicate_key", Details = details }; + public static Error InternalError(object details = null) => + new Error(1, "internal_error", details); + public static Error NotFound(object details = null) => + new Error(2, "not_found", details); + public static Error AlreadyDone(object details = null) => + new Error(3, "already_done", details); + public static Error InvalidOperation(object details = null) => + new Error(4, "invalid_operation", details); + public static Error DatabaseError(object details = null) => + new Error(5, "database_error", details); + public static Error DuplicateKey(object details = null) => + new Error(6, "duplicate_key", details); + public static Error AccessDenied(object details = null) => + new Error(7, "access_denied", details); } } diff --git a/Utils/OperationResult.cs b/Utils/OperationResult.cs index 83799bd..ed3284d 100644 --- a/Utils/OperationResult.cs +++ b/Utils/OperationResult.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using Serilog; namespace DDDCommon.Utils { @@ -31,6 +32,7 @@ public static async Task Try(Func func) } catch (Exception e) { + Log.Error(e, "OperationResult"); return new OperationResult(false, e as Error ?? Errors.InternalError(e.Message)); } @@ -44,6 +46,7 @@ public static async Task Try(Func> func) } catch (Exception e) { + Log.Error(e, "OperationResult"); return new OperationResult(false, e as Error ?? Errors.InternalError(e.Message)); } @@ -73,6 +76,7 @@ public static async Task> Try(Func> func) } catch (Exception e) { + Log.Error(e, "OperationResult<{T}>", typeof(T).FullName); return new OperationResult(false, default, e as Error ?? Errors.InternalError(e.Message)); } @@ -86,6 +90,7 @@ public static async Task> Try(Func>> } catch (Exception e) { + Log.Error(e, "OperationResult<{T}>", typeof(T).FullName); return new OperationResult(false, default, e as Error ?? Errors.InternalError(e.Message)); } @@ -96,7 +101,13 @@ public static async Task> Try(Func>> public class Error : Exception { public int Code { get; set; } - public string Text { get; set; } - public string Details { get; set; } + public object Details { get; set; } + + public Error(int code, string message, object details = null, Exception innerException = null) + : base(message, innerException) + { + Code = code; + Details = details; + } } } diff --git a/Utils/SerilogPostgreSqlSink/ColumnOptions.cs b/Utils/SerilogPostgreSqlSink/ColumnOptions.cs new file mode 100644 index 0000000..4c8da9e --- /dev/null +++ b/Utils/SerilogPostgreSqlSink/ColumnOptions.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Serilog.Sinks.PostgreSql +{ + public class ColumnOptions + { + public static IDictionary Default => new Dictionary + { + {DefaultColumnNames.RenderedMesssage, new RenderedMessageColumnWriter()}, + {DefaultColumnNames.MessageTemplate, new MessageTemplateColumnWriter()}, + {DefaultColumnNames.Level, new LevelColumnWriter()}, + {DefaultColumnNames.Exception, new ExceptionColumnWriter()}, + {DefaultColumnNames.Properties, new PropertiesColumnWriter()}, + {DefaultColumnNames.CreatedAt, new TimestampColumnWriter()} + }; + } + + public static class DefaultColumnNames + { + public const string RenderedMesssage = "message"; + public const string MessageTemplate = "message_template"; + public const string Level = "level"; + public const string Exception = "exception"; + public const string Properties = "properties"; + public const string CreatedAt = "created_at"; + } +} \ No newline at end of file diff --git a/Utils/SerilogPostgreSqlSink/ColumnWriterBase.cs b/Utils/SerilogPostgreSqlSink/ColumnWriterBase.cs new file mode 100644 index 0000000..749fa97 --- /dev/null +++ b/Utils/SerilogPostgreSqlSink/ColumnWriterBase.cs @@ -0,0 +1,248 @@ +using System; +using System.Text; +using NodaTime; +using NpgsqlTypes; +using Serilog.Events; +using Serilog.Formatting.Json; + +namespace Serilog.Sinks.PostgreSql +{ + public abstract class ColumnWriterBase + { + /// + /// Column type + /// + public NpgsqlDbType DbType { get; } + + protected ColumnWriterBase(NpgsqlDbType dbType) + { + DbType = dbType; + } + + /// + /// Gets part of log event to write to the column + /// + /// + /// + /// + public abstract object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null); + + } + + /// + /// Writes timestamp part + /// + public class TimestampColumnWriter : ColumnWriterBase + { + public TimestampColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Timestamp) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return Instant.FromDateTimeOffset(logEvent.Timestamp); + } + } + + /// + /// Writes message part + /// + public class RenderedMessageColumnWriter : ColumnWriterBase + { + public RenderedMessageColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Text) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return logEvent.RenderMessage(formatProvider); + } + } + + /// + /// Writes non rendered message + /// + public class MessageTemplateColumnWriter : ColumnWriterBase + { + public MessageTemplateColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Text) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return logEvent.MessageTemplate.Text; + } + } + + /// + /// Writes log level + /// + public class LevelColumnWriter : ColumnWriterBase + { + private readonly bool _renderAsText; + + public LevelColumnWriter(bool renderAsText = false, NpgsqlDbType dbType = NpgsqlDbType.Smallint) : base(dbType) + { + _renderAsText = renderAsText; + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + if (_renderAsText) + { + return logEvent.Level.ToString(); + } + + return (short)logEvent.Level; + } + } + + /// + /// Writes exception (just it ToString()) + /// + public class ExceptionColumnWriter : ColumnWriterBase + { + public ExceptionColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Text) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return logEvent.Exception == null ? (object)DBNull.Value : logEvent.Exception.ToString(); + } + } + + /// + /// Writes all event properties as json + /// + public class PropertiesColumnWriter : ColumnWriterBase + { + public PropertiesColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Jsonb) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return PropertiesToJson(logEvent); + } + + private object PropertiesToJson(LogEvent logEvent) + { + if (logEvent.Properties.Count == 0) + return "{}"; + + var valuesFormatter = new JsonValueFormatter(); + + var sb = new StringBuilder(); + + sb.Append("{"); + + using (var writer = new System.IO.StringWriter(sb)) + { + foreach (var logEventProperty in logEvent.Properties) + { + sb.Append($"\"{logEventProperty.Key}\":"); + + valuesFormatter.Format(logEventProperty.Value, writer); + + sb.Append(", "); + } + } + + sb.Remove(sb.Length - 2, 2); + sb.Append("}"); + + return sb.ToString(); + } + } + + /// + /// Writes log event as json + /// + public class LogEventSerializedColumnWriter : ColumnWriterBase + { + public LogEventSerializedColumnWriter(NpgsqlDbType dbType = NpgsqlDbType.Jsonb) : base(dbType) + { + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + return LogEventToJson(logEvent, formatProvider); + } + + private object LogEventToJson(LogEvent logEvent, IFormatProvider formatProvider) + { + var jsonFormatter = new JsonFormatter(formatProvider: formatProvider); + + var sb = new StringBuilder(); + using (var writer = new System.IO.StringWriter(sb)) + jsonFormatter.Format(logEvent, writer); + return sb.ToString(); + } + } + + /// + /// Write single event property + /// + public class SinglePropertyColumnWriter : ColumnWriterBase + { + public string Name { get; } + public PropertyWriteMethod WriteMethod { get; } + public string Format { get; } + + public SinglePropertyColumnWriter(string propertyName, PropertyWriteMethod writeMethod = PropertyWriteMethod.ToString, + NpgsqlDbType dbType = NpgsqlDbType.Text, string format = null) : base(dbType) + { + Name = propertyName; + WriteMethod = writeMethod; + Format = format; + } + + public override object GetValue(LogEvent logEvent, IFormatProvider formatProvider = null) + { + if (!logEvent.Properties.ContainsKey(Name)) + { + return DBNull.Value; + } + + switch (WriteMethod) + { + case PropertyWriteMethod.Raw: + return GetPropertyValue(logEvent.Properties[Name]); + case PropertyWriteMethod.Json: + var valuesFormatter = new JsonValueFormatter(); + + var sb = new StringBuilder(); + + using (var writer = new System.IO.StringWriter(sb)) + { + valuesFormatter.Format(logEvent.Properties[Name], writer); + } + + return sb.ToString(); + + default: + return logEvent.Properties[Name].ToString(Format, formatProvider); + } + + } + + private object GetPropertyValue(LogEventPropertyValue logEventProperty) + { + //TODO: Add support for arrays + if (logEventProperty is ScalarValue scalarValue) + { + return scalarValue.Value; + } + + return logEventProperty; + } + } + + public enum PropertyWriteMethod + { + Raw, + ToString, + Json + } +} \ No newline at end of file diff --git a/Utils/SerilogPostgreSqlSink/LoggerConfigurationPostgreSqlExtensions.cs b/Utils/SerilogPostgreSqlSink/LoggerConfigurationPostgreSqlExtensions.cs new file mode 100644 index 0000000..8746b5e --- /dev/null +++ b/Utils/SerilogPostgreSqlSink/LoggerConfigurationPostgreSqlExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Serilog; +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.PostgreSQL; + +namespace Serilog.Sinks.PostgreSql +{ + public static class LoggerConfigurationPostgreSqlExtensions + { + /// + /// Default time to wait between checking for event batches. + /// + public static readonly TimeSpan DefaultPeriod = TimeSpan.FromSeconds(5); + + /// + /// Adds a sink which writes to PostgreSQL table + /// + /// The logger configuration. + /// The connection string to the database where to store the events. + /// Name of the table to store the events in. + /// Table columns writers + /// The minimum log event level required in order to write an event to the sink. + /// The time to wait between checking for event batches. + /// Supplies culture-specific formatting information, or null. + /// The maximum number of events to include to single batch. + /// A switch allowing the pass-through minimum level to be changed at runtime. + /// If true inserts data via COPY command, otherwise uses INSERT INTO satement + /// Schema name + /// Set if sink should create table + /// Set if sink should auto quotate identifiers + /// Logger configuration, allowing configuration to continue. + public static LoggerConfiguration PostgreSql(this LoggerSinkConfiguration sinkConfiguration, + string connectionString, + string tableName, + IDictionary columnOptions = null, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + TimeSpan? period = null, + IFormatProvider formatProvider = null, + int batchSizeLimit = PostgreSqlSink.DefaultBatchSizeLimit, + LoggingLevelSwitch levelSwitch = null, + bool useCopy = true, + string schemaName = "", + bool needAutoCreateTable = false, + bool respectCase = false) + { + if (sinkConfiguration == null) + { + throw new ArgumentNullException(nameof(sinkConfiguration)); + } + + + period = period ?? DefaultPeriod; + + return sinkConfiguration.Sink(new PostgreSqlSink(connectionString, + tableName, + period.Value, + formatProvider, + columnOptions, + batchSizeLimit, + useCopy, + schemaName, + needAutoCreateTable, + respectCase), restrictedToMinimumLevel, levelSwitch); + } + } +} \ No newline at end of file diff --git a/Utils/SerilogPostgreSqlSink/PostgreSqlSink.cs b/Utils/SerilogPostgreSqlSink/PostgreSqlSink.cs new file mode 100644 index 0000000..59c0c26 --- /dev/null +++ b/Utils/SerilogPostgreSqlSink/PostgreSqlSink.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NodaTime; +using NodaTime.Extensions; +using Npgsql; +using Serilog.Events; +using Serilog.Sinks.PeriodicBatching; +using Serilog.Sinks.PostgreSQL; + +namespace Serilog.Sinks.PostgreSql +{ + public class PostgreSqlSink : PeriodicBatchingSink + { + private readonly string _connectionString; + + private readonly string _fullTableName; + private readonly IDictionary _columnOptions; + private readonly IFormatProvider _formatProvider; + private readonly bool _useCopy; + + public const int DefaultBatchSizeLimit = 30; + public const int DefaultQueueLimit = Int32.MaxValue; + + private bool _isTableCreated; + + + public PostgreSqlSink(string connectionString, + string tableName, + TimeSpan period, + IFormatProvider formatProvider = null, + IDictionary columnOptions = null, + int batchSizeLimit = DefaultBatchSizeLimit, + bool useCopy = true, + string schemaName = "", + bool needAutoCreateTable = false, + bool respectCase = false) : base(batchSizeLimit, period) + { + _connectionString = connectionString; + + if (respectCase) + { + schemaName = QuoteIdentifier(schemaName); + tableName = QuoteIdentifier(tableName); + } + + _fullTableName = GetFullTableName(tableName, schemaName); + + _formatProvider = formatProvider; + _useCopy = useCopy; + + _columnOptions = columnOptions ?? ColumnOptions.Default; + if (respectCase) + { + _columnOptions = CreateQuotedColumnsDict(_columnOptions); + } + + + _isTableCreated = !needAutoCreateTable; + } + + public PostgreSqlSink(string connectionString, + string tableName, + TimeSpan period, + IFormatProvider formatProvider = null, + IDictionary columnOptions = null, + int batchSizeLimit = DefaultBatchSizeLimit, + int queueLimit = DefaultQueueLimit, + bool useCopy = true, + string schemaName = "", + bool needAutoCreateTable = false, + bool respectCase = false) : base(batchSizeLimit, period, queueLimit) + { + _connectionString = connectionString; + + if (respectCase) + { + schemaName = QuoteIdentifier(schemaName); + tableName = QuoteIdentifier(tableName); + } + + _fullTableName = GetFullTableName(tableName, schemaName); + + _formatProvider = formatProvider; + _useCopy = useCopy; + + _columnOptions = columnOptions ?? ColumnOptions.Default; + if (respectCase) + { + _columnOptions = CreateQuotedColumnsDict(_columnOptions); + } + + + _isTableCreated = !needAutoCreateTable; + } + + private static string QuoteIdentifier(string identifier) + { + if (String.IsNullOrEmpty(identifier) || identifier.StartsWith("\"")) + { + return identifier; + } + + return $"\"{identifier}\""; + } + + private IDictionary CreateQuotedColumnsDict( + IDictionary originalColumnsDict) + { + var result = new Dictionary(originalColumnsDict.Count); + + foreach (var kvp in originalColumnsDict) + { + result[QuoteIdentifier(kvp.Key)] = kvp.Value; + } + + return result; + } + + private string GetFullTableName(string tableName, string schemaName) + { + var schemaPrefix = String.Empty; + if (!String.IsNullOrEmpty(schemaName)) + { + schemaPrefix = schemaName + "."; + } + + return schemaPrefix + tableName; + } + + + protected override async Task EmitBatchAsync(IEnumerable events) + { + using (var connection = new NpgsqlConnection(_connectionString)) + { + await connection.OpenAsync(); + + if (!_isTableCreated) + { + TableCreator.CreateTable(connection, _fullTableName, _columnOptions); + _isTableCreated = true; + } + + if (_useCopy) + { + ProcessEventsByCopyCommand(events, connection); + } + else + { + await ProcessEventsByInsertStatements(events, connection); + } + } + } + + private async Task ProcessEventsByInsertStatements(IEnumerable events, NpgsqlConnection connection) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = GetInsertQuery(); + + foreach (var col in _columnOptions) + { + command.Parameters.Add(ClearColumnNameForParameterName(col.Key), col.Value.DbType); + } + foreach (var logEvent in events) + { + foreach (var col in _columnOptions) + { + command.Parameters[col.Key].Value = col.Value.GetValue(logEvent, _formatProvider); + } + await command.ExecuteNonQueryAsync(); + } + } + } + + private static string ClearColumnNameForParameterName(string columnName) + { + return columnName?.Replace("\"", ""); + } + + private void ProcessEventsByCopyCommand(IEnumerable events, NpgsqlConnection connection) + { + using (var writer = connection.BeginBinaryImport(GetCopyCommand())) + { + + foreach (var e in events) + { + writer.StartRow(); + + foreach (var columnKey in _columnOptions.Keys) + { + writer.Write(_columnOptions[columnKey].GetValue(e, _formatProvider), + _columnOptions[columnKey].DbType); + } + } + + writer.Complete(); + } + } + + private string GetCopyCommand() + { + var columns = String.Join(", ", _columnOptions.Keys); + + return $"COPY {_fullTableName}({columns}) FROM STDIN BINARY;"; + + } + + private string GetInsertQuery() + { + var columns = String.Join(", ", _columnOptions.Keys); + + var parameters = String.Join(", ", + _columnOptions.Keys.Select(cn => ":" + ClearColumnNameForParameterName(cn))); + + return $@"INSERT INTO {_fullTableName} ({columns}) + VALUES ({parameters})"; + } + + private void WriteToStream(NpgsqlBinaryImporter writer, IEnumerable entities) + { + } + } +} \ No newline at end of file diff --git a/Utils/SerilogPostgreSqlSink/TableCreator.cs b/Utils/SerilogPostgreSqlSink/TableCreator.cs new file mode 100644 index 0000000..b4a0a72 --- /dev/null +++ b/Utils/SerilogPostgreSqlSink/TableCreator.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Npgsql; +using NpgsqlTypes; + +namespace Serilog.Sinks.PostgreSql +{ + public class TableCreator + { + public static int DefaultCharColumnsLength = 50; + public static int DefaultVarcharColumnsLength = 50; + public static int DefaultBitColumnsLength = 8; + + public static void CreateTable(NpgsqlConnection connection, string tableName, IDictionary columnsInfo) + { + using (var command = connection.CreateCommand()) + { + command.CommandText = GetCreateTableQuery(tableName, columnsInfo); + command.ExecuteNonQuery(); + } + } + + private static string GetCreateTableQuery(string tableName, IDictionary columnsInfo) + { + var builder = new StringBuilder("CREATE TABLE IF NOT EXISTS "); + builder.Append(tableName); + builder.AppendLine(" ("); + + builder.AppendLine(String.Join(",\n", columnsInfo.Select(r => $" {r.Key} {GetSqlTypeStr(r.Value.DbType)} "))); + + builder.AppendLine(")"); + + return builder.ToString(); + } + + private static string GetSqlTypeStr(NpgsqlDbType dbType) + { + switch (dbType) + { + case NpgsqlDbType.Bigint: + return "bigint"; + case NpgsqlDbType.Double: + return "double precision"; + case NpgsqlDbType.Integer: + return "integer"; + case NpgsqlDbType.Numeric: + return "numeric"; + case NpgsqlDbType.Real: + return "real"; + case NpgsqlDbType.Smallint: + return "smallint"; + case NpgsqlDbType.Boolean: + return "boolean"; + case NpgsqlDbType.Money: + return "money"; + case NpgsqlDbType.Char: + return $"character({DefaultCharColumnsLength})"; + case NpgsqlDbType.Text: + return "text"; + case NpgsqlDbType.Varchar: + return $"character varying({DefaultVarcharColumnsLength})"; + case NpgsqlDbType.Bytea: + return "bytea"; + case NpgsqlDbType.Date: + return "date"; + case NpgsqlDbType.Time: + return "time"; + case NpgsqlDbType.Timestamp: + return "timestamp"; + case NpgsqlDbType.TimestampTz: + return "timestamp with time zone"; + case NpgsqlDbType.Interval: + return "interval"; + case NpgsqlDbType.TimeTz: + return "time with time zone"; + case NpgsqlDbType.Inet: + return "inet"; + case NpgsqlDbType.Cidr: + return "cidr"; + case NpgsqlDbType.MacAddr: + return "macaddr"; + case NpgsqlDbType.Bit: + return $"bit({DefaultBitColumnsLength})"; + case NpgsqlDbType.Varbit: + return $"bit varying({DefaultBitColumnsLength})"; + case NpgsqlDbType.Uuid: + return "uuid"; + case NpgsqlDbType.Xml: + return "xml"; + case NpgsqlDbType.Json: + return "json"; + case NpgsqlDbType.Jsonb: + return "jsonb"; + default: + throw new ArgumentOutOfRangeException(nameof(dbType), dbType, "Cannot atomatically create column of type " + dbType); + } + } + } +} \ No newline at end of file