diff --git a/src/Abstractions/ISoftDeletable.cs b/src/Abstractions/ISoftDeletable.cs index 42f28a9..e647705 100644 --- a/src/Abstractions/ISoftDeletable.cs +++ b/src/Abstractions/ISoftDeletable.cs @@ -3,5 +3,9 @@ namespace Dgmjr.EntityFrameworkCore.Abstractions; public interface ISoftDeletable { DateTimeOffset? Deleted { get; set; } - bool IsDeleted { get; } +#if NET6_0_OR_GREATER + bool IsDeleted => Deleted is not null && Deleted.Value < DateTimeOffset.Now; +#else + bool IsDeleted { get; set; } +#endif } diff --git a/src/Constants/Constants/Constants.cs b/src/Constants/Constants/Constants.cs index 4939303..1d5655f 100644 --- a/src/Constants/Constants/Constants.cs +++ b/src/Constants/Constants/Constants.cs @@ -11,7 +11,7 @@ public static class Prefixes /// ufn_ public const string ufn_ = nameof(ufn_); - /// sp_ + /// udp_ public const string udp_ = nameof(udp_); /// sp_ diff --git a/src/Constants/DbTypesNamesAndSchemas/DbTypeNameEnum.cs b/src/Constants/DbTypesNamesAndSchemas/DbTypeNameEnum.cs index 731a4c9..7a7cff8 100644 --- a/src/Constants/DbTypesNamesAndSchemas/DbTypeNameEnum.cs +++ b/src/Constants/DbTypesNamesAndSchemas/DbTypeNameEnum.cs @@ -79,6 +79,9 @@ public enum DbTypeNames /// Database type of datetime. [Display(Name = "datetime", Description = "Database type of datetime.", ShortName = "datetime")] SqlDateTime, + /// Database type of datetime2. + [Display(Name = "datetime2", Description = "Database type of datetime2.", ShortName = "datetime")] + SqlDateTime2, /// Database type of date. [Display(Name = "date", Description = "Database type of date.", ShortName = "date")] @@ -122,5 +125,41 @@ public enum DbTypeNames Description = "Database type of uniqueidentifier.", ShortName = "uniqueidentifier" )] - UniqueIdentifier + UniqueIdentifier, + + /// Database type of float. + [Display( + Name = "float", + Description = "Database type of float.", + ShortName = "float" + )] + Float, + /// Database type of GEOMETRY. + [Display( + Name = "GEOMETRY", + Description = "Database type of GEOMETRY.", + ShortName = "GEOMETRY" + )] + Geometry, + /// Database type of GEOGRAPHY. + [Display( + Name = "GEOGRAPHY", + Description = "Database type of GEOGRAPHY.", + ShortName = "GEOGRAPHY" + )] + Geography, + /// Database type of VARCHAR({0}). + [Display( + Name = "VARCHAR({0})", + Description = "Database type of VARCHAR({0}).", + ShortName = "VARCHAR({0})" + )] + VarCharX, + /// Database type of NVARCHAR({0}). + [Display( + Name = "NVARCHAR({0})", + Description = "Database type of NVARCHAR({0}).", + ShortName = "NVARCHAR({0})" + )] + NVarCharX, } diff --git a/src/Constants/DbTypesNamesAndSchemas/Dgmjr.EntityFrameworkCore.Constants.DbTypesNamesAndSchemas.csproj b/src/Constants/DbTypesNamesAndSchemas/Dgmjr.EntityFrameworkCore.Constants.DbTypesNamesAndSchemas.csproj index 4638456..2eeabca 100644 --- a/src/Constants/DbTypesNamesAndSchemas/Dgmjr.EntityFrameworkCore.Constants.DbTypesNamesAndSchemas.csproj +++ b/src/Constants/DbTypesNamesAndSchemas/Dgmjr.EntityFrameworkCore.Constants.DbTypesNamesAndSchemas.csproj @@ -1,6 +1,7 @@ netstandard2.0 + Provides a set of constants and enums for EF Core @@ -10,5 +11,6 @@ + diff --git a/src/Constants/DbTypesNamesAndSchemas/SchemasEnum.cs b/src/Constants/DbTypesNamesAndSchemas/SchemasEnum.cs index f44e0ef..3388f8d 100644 --- a/src/Constants/DbTypesNamesAndSchemas/SchemasEnum.cs +++ b/src/Constants/DbTypesNamesAndSchemas/SchemasEnum.cs @@ -13,7 +13,7 @@ namespace Dgmjr.EntityFrameworkCore.Enums; [GenerateEnumerationRecordStruct("DbSchemas", "Dgmjr.EntityFrameworkCore")] -public enum SchemasEnum +public enum Schemas { [Display(Name = "DBO Schema", ShortName = "dbo", Description = "The dbo schema")] DboSchema, diff --git a/src/Migrations/Constants.cs b/src/Migrations/Constants.cs index b67b5d2..99c0773 100644 --- a/src/Migrations/Constants.cs +++ b/src/Migrations/Constants.cs @@ -1,11 +1,44 @@ +using System.Domain; +using System.Net.Mail; +using System.Runtime.InteropServices.ComTypes; + +using Dgmjr.EntityFrameworkCore.Constants; + +using Byte = Dgmjr.EntityFrameworkCore.DbTypeNames.Byte; + namespace Dgmjr.EntityFrameworkCore.Migrations; public static class Constants { - public const string CreateOrAlterFunctionPattern = - "CREATE OR ALTER FUNCTION {0}.{1}({2}) RETURNS BIT AS BEGIN {3} END"; + public const string CreateOrAlterFunctionPattern = """ + CREATE OR ALTER FUNCTION {0}.{1}({2}) + RETURNS {3} + AS + BEGIN + {4} + END + """; + public const string CreateOrAlterViewPattern = """ + CREATE OR ALTER VIEW {0}.{1} + AS + {2} + """; + public const string CreateOrAlterProcedurePattern = """ + CREATE OR ALTER PROCEDURE {0}.{1} {2} + AS + BEGIN + {3} + END + """; + + public const string Function = "FUNCTION"; + public const string Procedure = "PROCEDURE"; + public const string View = "VIEW"; public const string DropFunctionIfExistsPattern = "DROP FUNCTION IF EXISTS {0}.{1}"; + public const string DropProcedureIfExistsPattern = "DROP PROCEDURE IF EXISTS {0}.{1}"; + public const string DropViewIfExistsPattern = "DROP VIEW IF EXISTS {0}.{1}"; + public const string DropSomethingIfExistsPattern = "DROP {0} IF EXISTS {1}.{2}"; public static readonly IReadOnlyDictionary ClrTypeToSqlTypeMap = new Dictionary< Type, @@ -15,7 +48,44 @@ public static class Constants { typeof(string), NVarCharMax.ShortName }, { typeof(int), Int.ShortName }, { typeof(long), BigInt.ShortName }, - { typeof(datetime), SqlDateTime.ShortName }, - { typeof(DateTimeOffset), SqlDateTimeOffset.ShortName } + { typeof(DateTimeOffset), SqlDateTimeOffset.ShortName }, + { typeof(guid), UniqueIdentifier.ShortName }, + { typeof(bool), Bit.ShortName }, + { typeof(decimal), Float.ShortName }, + { typeof(double), Float.ShortName }, + { typeof(float), Float.ShortName }, + { typeof(byte), Byte.ShortName }, + { typeof(short), TinyInt.ShortName }, + { typeof(byte[]), VarBinaryMax.ShortName }, + { typeof(DateTime), Date.ShortName }, + { typeof(DateTime?), Date.ShortName }, + { typeof(DateTimeOffset?), SqlDateTime2.ShortName }, + { typeof(TimeSpan), Time.ShortName }, + { typeof(TimeSpan?), Time.ShortName }, + { typeof(decimal?), Float.ShortName }, + { typeof(double?), Float.ShortName }, + { typeof(float?), Float.ShortName }, + { typeof(byte?), Byte.ShortName }, + { typeof(short?), TinyInt.ShortName }, + { typeof(int?), Int.ShortName }, + { typeof(long?), BigInt.ShortName }, + { typeof(bool?), Bit.ShortName }, + { typeof(NetTopologySuite.Geometries.Point), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.LineString), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.Polygon), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.MultiPoint), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.MultiLineString), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.MultiPolygon), Geography.ShortName }, + { typeof(NetTopologySuite.Geometries.GeometryCollection), Geography.ShortName }, + { typeof(Slug), Format(CharX.DisplayName, 6) }, + { typeof(Uri), NVarCharMax.DisplayName }, + { typeof(uri), NVarCharMax.DisplayName }, + { typeof(urn), NVarCharMax.DisplayName }, + { typeof(url), NVarCharMax.DisplayName }, + { typeof(xri), NVarCharMax.DisplayName }, + { typeof(EmailAddress), Format(NVarCharX.DisplayName, UriMaxLengthConstant.UriMaxLength) }, + { typeof(PhoneNumber), Format(VarCharX.DisplayName, 24) }, + { typeof(EmailAddress?), Format(NVarCharX.DisplayName, UriMaxLengthConstant.UriMaxLength) }, + { typeof(PhoneNumber?), Format(VarCharX.DisplayName, 24) }, }; } diff --git a/src/Migrations/CreateFunctionOperation.cs b/src/Migrations/CreateFunctionOperation.cs index 35de21f..7f9e5c1 100644 --- a/src/Migrations/CreateFunctionOperation.cs +++ b/src/Migrations/CreateFunctionOperation.cs @@ -2,31 +2,34 @@ namespace Dgmjr.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; -public class CreateFunctionOperation(string schema, string name, string arguments, string body) +public class CreateFunctionOperation(string schema, string name, SqlArgument[] arguments, string returnType, string body) : SqlOperation { public string Schema { get; set; } = schema; public string Name { get; set; } = name; - public string Arguments { get; set; } = arguments; + public SqlArgument[] Arguments { get; set; } = arguments; public string Body { get; set; } = body; + public string ReturnType { get; set; } = returnType; public override string Sql => - Format(CreateOrAlterFunctionPattern, Schema, Name, Arguments, Body); + Format(CreateOrAlterFunctionPattern, Schema, Name, Arguments.Join(", "), ReturnType, Body); public CreateFunctionOperation( MethodInfo mi, string body, string schema = DboSchema.ShortName, string? name = default, - string? arguments = default + SqlArgument[]? arguments = default ) : this( schema, name ?? mi.Name, arguments ?? mi.GetParameters() - .Select(p => $"@{p.Name} {ClrTypeToSqlTypeMap[p.ParameterType]}") - .Join(", "), + // .Select(p => $"@{p.Name} {ClrTypeToSqlTypeMap[p.ParameterType]}").ToArray(), + .Select(arg => (SqlArgument)arg) + .ToArray(), + ClrTypeToSqlTypeMap[mi.ReturnType], body ) { } } diff --git a/src/Migrations/CreateProcedureOperation.cs b/src/Migrations/CreateProcedureOperation.cs new file mode 100644 index 0000000..cd94493 --- /dev/null +++ b/src/Migrations/CreateProcedureOperation.cs @@ -0,0 +1,32 @@ +namespace Dgmjr.EntityFrameworkCore.Migrations; + +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +public class CreateProcedureOperation(string schema, string name, SqlArgument[] arguments, string body) + : SqlOperation +{ + public string Schema { get; set; } = schema; + public string Name { get; set; } = name; + public SqlArgument[] Arguments { get; set; } = arguments; + public string Body { get; set; } = body; + + public override string Sql => + Format(CreateOrAlterProcedurePattern, Schema, Name, Arguments.Join(", "), Body); + + public CreateProcedureOperation( + MethodInfo mi, + string body, + string schema = DboSchema.ShortName, + string? name = default, + SqlArgument[]? arguments = default + ) + : this( + schema, + name ?? mi.Name, + arguments + ?? mi.GetParameters() + .Select(arg => (SqlArgument)arg) + .ToArray(), + body + ) { } +} diff --git a/src/Migrations/CreateViewOperation.cs b/src/Migrations/CreateViewOperation.cs index c5f8bbb..2e5c9d8 100644 --- a/src/Migrations/CreateViewOperation.cs +++ b/src/Migrations/CreateViewOperation.cs @@ -1,11 +1,10 @@ -// namespace Dgmjr.EntityFrameworkCore.Migrations; +namespace Dgmjr.EntityFrameworkCore.Migrations; -// public class CreateViewOperation(string schema, string name, params Column[] columns) : SqlOperation -// { -// public string Schema { get; set; } = schema; -// public string Name { get; set; } = name; -// public Column[] Columns { get; set; } = columns; -// public Expression -// } +public class CreateViewOperation(string schema, string name, string selectStatement) : SqlOperation +{ + public string Schema { get; set; } = schema; + public string Name { get; set; } = name; + public string SelectStatement { get; set; } = selectStatement; -// public record struct Column(string DataType, string SourceTable, string ColumnName); + public override string Sql => Format(CreateOrAlterViewPattern, Schema, Name, SelectStatement); +} diff --git a/src/Migrations/CustomSqlMigrationsGenerator.cs b/src/Migrations/CustomSqlMigrationsGenerator.cs index a0906ad..9083d5f 100644 --- a/src/Migrations/CustomSqlMigrationsGenerator.cs +++ b/src/Migrations/CustomSqlMigrationsGenerator.cs @@ -29,7 +29,7 @@ ICommandBatchPreparer commandBatchPreparer protected override void Generate( MigrationOperation operation, - IModel model, + IModel? model, MigrationCommandListBuilder builder ) { @@ -41,6 +41,22 @@ MigrationCommandListBuilder builder { builder.Append(dfo.Sql).Append(SqlHelper.StatementTerminator).EndCommand(); } + else if (operation is DropViewOperation dvo) + { + builder.Append(dvo.Sql).Append(SqlHelper.StatementTerminator).EndCommand(); + } + else if (operation is CreateViewOperation cvo) + { + builder.Append(cvo.Sql).Append(SqlHelper.StatementTerminator).EndCommand(); + } + else if (operation is DropOperation dropop) + { + builder.Append(dropop.Sql).Append(SqlHelper.StatementTerminator).EndCommand(); + } + else if (operation is CreateProcedureOperation cpop) + { + builder.Append(cpop.Sql).Append(SqlHelper.StatementTerminator).EndCommand(); + } else { base.Generate(operation, model, builder); diff --git a/src/Migrations/Dgmjr.EntityFrameworkCore.Migrations.csproj b/src/Migrations/Dgmjr.EntityFrameworkCore.Migrations.csproj index fde521a..21f16b6 100644 --- a/src/Migrations/Dgmjr.EntityFrameworkCore.Migrations.csproj +++ b/src/Migrations/Dgmjr.EntityFrameworkCore.Migrations.csproj @@ -1,12 +1,15 @@ netstandard2.0;netstandard2.1;net6.0;net8.0 + Entity Framework Core Migrations - - + + + + diff --git a/src/Migrations/DropFunctionOperation.cs b/src/Migrations/DropFunctionOperation.cs index 5e76bbf..13e48cb 100644 --- a/src/Migrations/DropFunctionOperation.cs +++ b/src/Migrations/DropFunctionOperation.cs @@ -2,11 +2,6 @@ namespace Dgmjr.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Operations; -public class DropFunctionOperation(string schema, string name) : SqlOperation +public class DropFunctionOperation(string schema, string name) : DropOperation(Function, schema, name) { - public string Schema { get; set; } = schema; - public string Name { get; set; } = name; - public override bool IsDestructiveChange => true; - - public override string Sql => Format(DropFunctionIfExistsPattern, Schema, Name); } diff --git a/src/Migrations/DropOperation.cs b/src/Migrations/DropOperation.cs new file mode 100644 index 0000000..13630f3 --- /dev/null +++ b/src/Migrations/DropOperation.cs @@ -0,0 +1,11 @@ +namespace Dgmjr.EntityFrameworkCore.Migrations; + +public class DropOperation(string thingKind, string schema, string name) : SqlOperation +{ + public string ThingKind { get; set; } = thingKind; + public string Schema { get; set; } = schema; + public string Name { get; set; } = name; + public override bool IsDestructiveChange => true; + + public override string Sql => Format(DropSomethingIfExistsPattern, ThingKind, Schema, Name); +} diff --git a/src/Migrations/DropProcedureOperation.cs b/src/Migrations/DropProcedureOperation.cs new file mode 100644 index 0000000..ae39fb3 --- /dev/null +++ b/src/Migrations/DropProcedureOperation.cs @@ -0,0 +1,8 @@ +namespace Dgmjr.EntityFrameworkCore.Migrations; + +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +public class DropProcedureOperation(string schema, string name) : DropOperation(Procedure, schema, name) +{ + +} diff --git a/src/Migrations/DropViewOperation.cs b/src/Migrations/DropViewOperation.cs new file mode 100644 index 0000000..36d9d63 --- /dev/null +++ b/src/Migrations/DropViewOperation.cs @@ -0,0 +1,7 @@ +namespace Dgmjr.EntityFrameworkCore.Migrations; + +using Microsoft.EntityFrameworkCore.Migrations.Operations; + +public class DropViewOperation(string schema, string name) : DropOperation(View, schema, name) +{ +} diff --git a/src/Migrations/MigrationBuilderExtensions.cs b/src/Migrations/MigrationBuilderExtensions.cs new file mode 100644 index 0000000..838f0d1 --- /dev/null +++ b/src/Migrations/MigrationBuilderExtensions.cs @@ -0,0 +1,41 @@ +namespace Dgmjr.EntityFrameworkCore.Migrations; + +public static class MigrationBuilderExtensions +{ + public static void CreateView(this MigrationBuilder migrationBuilder, string schema, string name, string selectStatement) + { + migrationBuilder.Operations.Add(new CreateViewOperation(schema, name, selectStatement)); + } + + public static void DropView(this MigrationBuilder migrationBuilder, string schema, string name) + { + migrationBuilder.Operations.Add(new DropViewOperation(schema, name)); + } + + public static void DropProcedure(this MigrationBuilder migrationBuilder, string schema, string name) + { + migrationBuilder.Operations.Add(new DropProcedureOperation(schema, name)); + } + + public static void DropFunction(this MigrationBuilder migrationBuilder, string schema, string name) + { + migrationBuilder.Operations.Add(new DropFunctionOperation(schema, name)); + } + + public static void CreateProcedure(this MigrationBuilder migrationBuilder, string schema, string name, string[] args, string sql) + { + migrationBuilder.Operations.Add(new CreateProcedureOperation(schema, name, args.Select(arg => (SqlArgument)arg).ToArray(), sql)); + } + public static void CreateProcedure(this MigrationBuilder migrationBuilder, MethodInfo mi, string schema, string name, string sql) + { + migrationBuilder.Operations.Add(new CreateProcedureOperation(mi, sql, schema, name)); + } + public static void CreateFunction(this MigrationBuilder migrationBuilder, string schema, string name, string[] args, string returnType, string sql) + { + migrationBuilder.Operations.Add(new CreateFunctionOperation(schema, name, args.Select(arg => (SqlArgument)arg).ToArray(), returnType, sql)); + } + public static void CreateFunction(this MigrationBuilder migrationBuilder, MethodInfo mi, string schema, string name, string sql) + { + migrationBuilder.Operations.Add(new CreateFunctionOperation(mi, sql, schema, name)); + } +} diff --git a/src/Migrations/SqlArgument.cs b/src/Migrations/SqlArgument.cs new file mode 100644 index 0000000..959871b --- /dev/null +++ b/src/Migrations/SqlArgument.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace Dgmjr.EntityFrameworkCore.Migrations; + +[RegexDto(SqlArgumentRegex)] +[StructLayout(LayoutKind.Auto)] +public readonly partial struct SqlArgument +{ + private readonly string _name; + public string Name { get => _name; init => _name = value.StartsWith("@") ? value : "@" + value; } + public string Type { get; } + public string? Value { get; } + + public override string ToString() => $"{Name} {Type}{(Value is not null ? $" = {Value}" : "")}"; + + public static implicit operator string(SqlArgument arg) => arg.ToString(); + + public SqlArgument(string name, string type, string? value = null) => + (Name, Type, Value) = (name, type, value); + + public static implicit operator SqlArgument((string name, string type, string? value) arg) => + new(arg.name, arg.type, arg.value); + + public static implicit operator SqlArgument((string name, string type) arg) => + new(arg.name, arg.type); + + public static implicit operator SqlArgument(string arg) => + new(arg, ""); + + public static implicit operator SqlArgument(ParameterInfo arg) => + new(arg.Name, ClrTypeToSqlTypeMap[arg.ParameterType]); + + private const string SqlArgumentRegex = @"(?@\w+)\s+(?\w+)(\s*=\s*(?.*))?"; +} diff --git a/src/Models/Slug.cs b/src/Models/Slug.cs index 91b7e14..30a157e 100644 --- a/src/Models/Slug.cs +++ b/src/Models/Slug.cs @@ -3,12 +3,12 @@ namespace Dgmjr.EntityFrameworkCore; using Vogen; using Microsoft.EntityFrameworkCore.Metadata.Builders; -public readonly partial struct Slug(string Value) : IComparable, IEquatable, IComparable +public readonly partial struct Slug(string? Value = default) : IComparable, IEquatable, IComparable { public Slug() : this(NewSlug()) { } - public string Value { get; init; } = Value; + public string Value { get; init; } = Value ?? NewSlug(); public int CompareTo(Slug other) => Compare(Value, other.Value, OrdinalIgnoreCase); @@ -28,11 +28,26 @@ public bool Equals(Slug other) public override bool Equals(object? obj) => obj is Slug other && Equals(other); + public override int GetHashCode() + => Value.GetHashCode(); + public static bool operator==(Slug left, Slug right) => left.Equals(right); public static bool operator!=(Slug left, Slug right) => !left.Equals(right); + + public static bool operator<(Slug left, Slug right) + => left.CompareTo(right) < 0; + + public static bool operator<=(Slug left, Slug right) + => left.CompareTo(right) <= 0; + + public static bool operator>(Slug left, Slug right) + => left.CompareTo(right) > 0; + + public static bool operator>=(Slug left, Slug right) + => left.CompareTo(right) >= 0; } public class SlugEfCoreConverter : ValueConverter