diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c560808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,262 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc +/Hotel/static/public/images/hotel/1 diff --git a/DDDCommon.csproj b/DDDCommon.csproj new file mode 100644 index 0000000..e39fe5c --- /dev/null +++ b/DDDCommon.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + Majid Hatami + Meteor + true + latest + + + + + + + + + + + + diff --git a/Domain/Interfaces/IAggregateRoot.cs b/Domain/Interfaces/IAggregateRoot.cs new file mode 100644 index 0000000..7ec6436 --- /dev/null +++ b/Domain/Interfaces/IAggregateRoot.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Interfaces +{ + public interface IAggregateRoot : IEntity + { + } +} diff --git a/Domain/Interfaces/ICommand.cs b/Domain/Interfaces/ICommand.cs new file mode 100644 index 0000000..e63a818 --- /dev/null +++ b/Domain/Interfaces/ICommand.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Interfaces +{ + public interface ICommand + { + } +} diff --git a/Domain/Interfaces/IEntity.cs b/Domain/Interfaces/IEntity.cs new file mode 100644 index 0000000..17468c0 --- /dev/null +++ b/Domain/Interfaces/IEntity.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Interfaces +{ + public interface IEntity + { + } +} diff --git a/Domain/Interfaces/IEvent.cs b/Domain/Interfaces/IEvent.cs new file mode 100644 index 0000000..9bb9716 --- /dev/null +++ b/Domain/Interfaces/IEvent.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Interfaces +{ + public interface IEvent + { + } +} diff --git a/Domain/Interfaces/IEventBus.cs b/Domain/Interfaces/IEventBus.cs new file mode 100644 index 0000000..f5fe953 --- /dev/null +++ b/Domain/Interfaces/IEventBus.cs @@ -0,0 +1,13 @@ +using DDDCommon.Domain.Types; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DDDCommon.Domain.Interfaces +{ + public interface IEventBus + { + void Dispatch(DomainEvent notification); + } +} diff --git a/Domain/Types/AggregateRoot.cs b/Domain/Types/AggregateRoot.cs new file mode 100644 index 0000000..f86cfad --- /dev/null +++ b/Domain/Types/AggregateRoot.cs @@ -0,0 +1,14 @@ +using DDDCommon.Domain.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Types +{ + public class AggregateRoot : Entity, IAggregateRoot + { + public AggregateRoot(TId id) : base(id) + { + } + } +} diff --git a/Domain/Types/DomainCommand.cs b/Domain/Types/DomainCommand.cs new file mode 100644 index 0000000..e7d86ae --- /dev/null +++ b/Domain/Types/DomainCommand.cs @@ -0,0 +1,11 @@ +using DDDCommon.Domain.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Types +{ + public class DomainCommand : ICommand + { + } +} diff --git a/Domain/Types/DomainEvent.cs b/Domain/Types/DomainEvent.cs new file mode 100644 index 0000000..f13f24d --- /dev/null +++ b/Domain/Types/DomainEvent.cs @@ -0,0 +1,9 @@ +using DDDCommon.Domain.Interfaces; +using System; + +namespace DDDCommon.Domain.Types +{ + public abstract class DomainEvent : IEvent + { + } +} \ No newline at end of file diff --git a/Domain/Types/Entity.cs b/Domain/Types/Entity.cs new file mode 100644 index 0000000..b8be88c --- /dev/null +++ b/Domain/Types/Entity.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; + +namespace DDDCommon.Domain.Types +{ + public abstract class Entity : IEquatable> + { + public TId Id { get; protected set; } + + protected Entity(TId id) + { + //if (object.Equals(id, default(TId))) + //{ + // throw new ArgumentException("The ID cannot be the default value.", nameof(id)); + //} + + Id = id; + } + + public override bool Equals(object obj) + { + if (obj is Entity entity) + { + return Equals(entity); + } + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + #region IEquatable Members + + public bool Equals(Entity other) + { + if (other == null) + { + return false; + } + return Id.Equals(other.Id); + } + + #endregion + } +} \ No newline at end of file diff --git a/Domain/Types/IgnoreMemberAttribute.cs b/Domain/Types/IgnoreMemberAttribute.cs new file mode 100644 index 0000000..39f0a86 --- /dev/null +++ b/Domain/Types/IgnoreMemberAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace DDDCommon.Domain.Types +{ + // source: https://github.com/jhewlett/ValueObject + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class IgnoreMemberAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Domain/Types/ModelState.cs b/Domain/Types/ModelState.cs new file mode 100644 index 0000000..690804e --- /dev/null +++ b/Domain/Types/ModelState.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Domain.Types +{ + public enum ModelStates + { + None = 0, + Added = 1, + Updated = 2, + Removed = 3 + } + public class ModelState : ValueObject + { + public T Model { get; } + public ModelStates State { get; } + + public ModelState(T model, ModelStates state = ModelStates.None) + { + Model = model; + State = state; + } + + public ModelState NewState(ModelStates newState) + { + return new ModelState(Model, newState); + } + } +} diff --git a/Domain/Types/ValueObject.cs b/Domain/Types/ValueObject.cs new file mode 100644 index 0000000..8728cee --- /dev/null +++ b/Domain/Types/ValueObject.cs @@ -0,0 +1,114 @@ +using DDDCommon.Domain.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace DDDCommon.Domain.Types +{ + // source: https://github.com/jhewlett/ValueObject + public abstract class ValueObject : IEquatable + { + private List properties; + private List fields; + + public static bool operator ==(ValueObject obj1, ValueObject obj2) + { + if (Equals(obj1, null)) + { + if (Equals(obj2, null)) + { + return true; + } + return false; + } + return obj1.Equals(obj2); + } + + public static bool operator !=(ValueObject obj1, ValueObject obj2) + { + return !(obj1 == obj2); + } + + public bool Equals(ValueObject obj) + { + return Equals(obj as object); + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) return false; + + return GetProperties().All(p => PropertiesAreEqual(obj, p)) + && GetFields().All(f => FieldsAreEqual(obj, f)); + } + + private bool PropertiesAreEqual(object obj, PropertyInfo p) + { + return Equals(p.GetValue(this, null), p.GetValue(obj, null)); + } + + private bool FieldsAreEqual(object obj, FieldInfo f) + { + return Equals(f.GetValue(this), f.GetValue(obj)); + } + + private IEnumerable GetProperties() + { + if (properties == null) + { + properties = GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null) + .ToList(); + + // Not available in Core + // !Attribute.IsDefined(p, typeof(IgnoreMemberAttribute))).ToList(); + } + + return properties; + } + + private IEnumerable GetFields() + { + if (fields == null) + { + fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetCustomAttribute(typeof(IgnoreMemberAttribute)) == null) + .ToList(); + } + + return fields; + } + + public override int GetHashCode() + { + unchecked // allow overflow + { + int hash = 17; + foreach (var prop in GetProperties()) + { + var value = prop.GetValue(this, null); + hash = HashValue(hash, value); + } + + foreach (var field in GetFields()) + { + var value = field.GetValue(this); + hash = HashValue(hash, value); + } + + return hash; + } + } + + private int HashValue(int seed, object value) + { + var currentHash = value != null + ? value.GetHashCode() + : 0; + + return seed * 23 + currentHash; + } + } +} \ No newline at end of file diff --git a/Infrastructure/Interfaces/IRepository.cs b/Infrastructure/Interfaces/IRepository.cs new file mode 100644 index 0000000..e3e45bb --- /dev/null +++ b/Infrastructure/Interfaces/IRepository.cs @@ -0,0 +1,17 @@ +using DDDCommon.Domain.Interfaces; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DDDCommon.Infrastructure.Interfaces +{ + public interface IRepository where T : class, IEntity + { + Task GetBy(TId id); + Task> Get(int skip, int take); + Task Add(T entity); + Task Update(T entity); + Task Remove(T entity); + } +} diff --git a/Infrastructure/Types/DbConfigurations.cs b/Infrastructure/Types/DbConfigurations.cs new file mode 100644 index 0000000..5d4da8d --- /dev/null +++ b/Infrastructure/Types/DbConfigurations.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace DDDCommon.Infrastructure.Types +{ + public class DbConfigurations + { + public List EntityTypeAssemblies { get; set; } = new List(); + public List Schemas { get; set; } = new List(); + public string ConnectionString { get; set; } + public bool UseNodaTime { get; set; } + public bool UseNetTopologySuite { get; set; } + } +} diff --git a/Infrastructure/Types/EventBus.cs b/Infrastructure/Types/EventBus.cs new file mode 100644 index 0000000..6535dc4 --- /dev/null +++ b/Infrastructure/Types/EventBus.cs @@ -0,0 +1,22 @@ +using DDDCommon.Domain.Interfaces; +using DDDCommon.Domain.Types; +using System; +using System.Collections.Generic; +using System.Text; + +namespace DDDCommon.Infrastructure.Types +{ + public class EventBus : IEventBus + { + private readonly Queue _bus = new Queue(); + public int Count => _bus.Count; + + public void Dispatch(DomainEvent @event) + { + _bus.Enqueue(@event); + } + + public DomainEvent DequeueEvent() => _bus.Dequeue(); + } + +} diff --git a/Infrastructure/Types/PostgreSqlDbContext.cs b/Infrastructure/Types/PostgreSqlDbContext.cs new file mode 100644 index 0000000..f0b81f3 --- /dev/null +++ b/Infrastructure/Types/PostgreSqlDbContext.cs @@ -0,0 +1,187 @@ +using DDDCommon.Utils; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql; +using Npgsql.NameTranslation; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace DDDCommon.Infrastructure.Types +{ + public class PostgreSqlDbContext : DbContext + { + private static readonly Regex KeysRegex = new Regex("^(PK|FK|IX)_", RegexOptions.Compiled); + private readonly EventBus _bus; + private readonly IMediator _mediator; + private readonly DbConfigurations _dbConfigurations; + //[Obsolete] + //public static readonly LoggerFactory MyLoggerFactory + // = new LoggerFactory(new[] { new ConsoleLoggerProvider((category, level) + // => category == DbLoggerCategory.Database.Command.Name, true) }); + + public PostgreSqlDbContext(DbContextOptions options, + EventBus bus, IMediator mediator, DbConfigurations dbConfigurations) + : base(options) + { + _bus = bus; + _mediator = mediator; + _dbConfigurations = dbConfigurations; + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + //.UseLoggerFactory(MyLoggerFactory) + .EnableSensitiveDataLogging() + .UseNpgsql(_dbConfigurations.ConnectionString, o => + { + if (_dbConfigurations.UseNodaTime) + o.UseNodaTime(); + + if (_dbConfigurations.UseNetTopologySuite) + o.UseNetTopologySuite(); + }); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .ForNpgsqlUseIdentityByDefaultColumns() + .HasPostgresExtension("citext"); + + if (_dbConfigurations.UseNetTopologySuite) + modelBuilder.HasPostgresExtension("postgis"); + + _dbConfigurations.EntityTypeAssemblies.ForEach(x => modelBuilder.ApplyConfigurationsFromAssembly(x)); + + FixSnakeCaseNames(modelBuilder); + + Console.WriteLine("\n\nOn Model Creating!\n\n"); + } + + public void EnsureCreated() + { + Database.EnsureCreated(); + if (!(Database.GetService() is RelationalDatabaseCreator databaseCreator)) + return; + var schemas = $"'{string.Join("', '", _dbConfigurations.Schemas)}'"; + var sql = "SELECT count(*) FROM information_schema.tables " + + $"WHERE table_schema IN ({schemas}) AND table_type = 'BASE TABLE' AND table_name NOT IN('__EFMigrationsHistory', 'spatial_ref_sys');"; + Database.OpenConnection(); + using (var cmd = Database.GetDbConnection().CreateCommand()) + { + cmd.CommandText = sql; + if (Convert.ToInt64(cmd.ExecuteScalar()) == 0) + databaseCreator.CreateTables(); + } + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + + while (_bus.Count > 0) + { + var @event = _bus.DequeueEvent(); + await _mediator.Publish(@event, cancellationToken); + } + try + { + return await base.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException e) + { + if (!(e.InnerException is PostgresException pgError)) + throw Errors.InternalError(e.InnerException != null + ? e.InnerException.Message + : e.Message); + + switch (pgError.SqlState) + { + case "23505": + throw Errors.DuplicateKey(pgError.Detail); + default: + throw Errors.DatabaseError(pgError.Detail); + } + } + catch (Exception e) + { + throw Errors.InternalError(e.InnerException != null + ? e.InnerException.Message + : e.Message); + } + } + + private void FixSnakeCaseNames(ModelBuilder modelBuilder) + { + var mapper = new NpgsqlSnakeCaseNameTranslator(); + foreach (var table in modelBuilder.Model.GetEntityTypes()) + { + ConvertToSnake(mapper, table); + foreach (var property in table.GetProperties()) + { + ConvertToSnake(mapper, property); + } + + foreach (var primaryKey in table.GetKeys()) + { + ConvertToSnake(mapper, primaryKey); + } + + foreach (var foreignKey in table.GetForeignKeys()) + { + ConvertToSnake(mapper, foreignKey); + } + + foreach (var indexKey in table.GetIndexes()) + { + ConvertToSnake(mapper, indexKey); + } + } + } + + private void ConvertToSnake(INpgsqlNameTranslator mapper, object entity) + { + switch (entity) + { + case IMutableEntityType table: + var relationalTable = table.Relational(); + relationalTable.TableName = ConvertGeneralToSnake(mapper, relationalTable.TableName); + if (relationalTable.TableName.StartsWith("asp_net_")) + { + relationalTable.TableName = relationalTable.TableName.Replace("asp_net_", string.Empty); + relationalTable.Schema = "identity"; + } + + break; + case IMutableProperty property: + property.Relational().ColumnName = ConvertGeneralToSnake(mapper, property.Relational().ColumnName); + break; + case IMutableKey primaryKey: + primaryKey.Relational().Name = ConvertKeyToSnake(mapper, primaryKey.Relational().Name); + break; + case IMutableForeignKey foreignKey: + foreignKey.Relational().Name = ConvertKeyToSnake(mapper, foreignKey.Relational().Name); + break; + case IMutableIndex indexKey: + indexKey.Relational().Name = ConvertKeyToSnake(mapper, indexKey.Relational().Name); + break; + default: + throw new NotImplementedException("Unexpected type was provided to snake case converter"); + } + } + + private string ConvertKeyToSnake(INpgsqlNameTranslator mapper, string keyName) => + ConvertGeneralToSnake(mapper, KeysRegex.Replace(keyName, match => match.Value.ToLower())); + + private string ConvertGeneralToSnake(INpgsqlNameTranslator mapper, string entityName) => + mapper.TranslateMemberName(ModifyNameBeforeConvertion(mapper, entityName)); + + protected virtual string ModifyNameBeforeConvertion(INpgsqlNameTranslator mapper, string entityName) => entityName; + + } +} diff --git a/Infrastructure/Types/Repository.cs b/Infrastructure/Types/Repository.cs new file mode 100644 index 0000000..a6340e3 --- /dev/null +++ b/Infrastructure/Types/Repository.cs @@ -0,0 +1,52 @@ +using DDDCommon.Domain.Interfaces; +using DDDCommon.Domain.Types; +using DDDCommon.Infrastructure.Interfaces; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DDDCommon.Infrastructure.Types +{ + public class Repository : IRepository where T : class, IEntity + { + protected readonly PostgreSqlDbContext DbContext; + + public Repository(PostgreSqlDbContext dbContext) + { + DbContext = dbContext; + } + + public virtual Task GetBy(TId id) + { + return DbSet.SingleOrDefaultAsync(e => (e as Entity).Id.Equals(id)); + } + + public DbSet DbSet => DbContext.Set(); + + public virtual Task> Get(int skip, int take) + { + return DbSet.Skip(skip).Take(take).ToListAsync(); + } + + public virtual Task Add(T entity) + { + DbSet.Add(entity); + return DbContext.SaveChangesAsync(); + } + + public virtual Task Update(T entity) + { + DbContext.Entry(entity).State = EntityState.Modified; + return DbContext.SaveChangesAsync(); + } + + public virtual Task Remove(T entity) + { + DbSet.Remove(entity); + return DbContext.SaveChangesAsync(); + } + } +} diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..54ccf6a --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + FileSystem + Release + Any CPU + netstandard2.0 + bin\Debug\netstandard2.0\publish\ + + \ No newline at end of file diff --git a/Utils/ErrorJsonConverter.cs b/Utils/ErrorJsonConverter.cs new file mode 100644 index 0000000..893b609 --- /dev/null +++ b/Utils/ErrorJsonConverter.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace DDDCommon.Utils +{ + public class ErrorJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => + objectType == typeof(Error); + + public override bool CanRead => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => + throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (!(value is Error error)) + throw new JsonSerializationException("expected Error object"); + + var o = new JObject + { + { "code", error.Code }, + { "text", error.Text }, + { "details", error.Details } + }; + o.WriteTo(writer); + } + } + +} diff --git a/Utils/Errors.cs b/Utils/Errors.cs new file mode 100644 index 0000000..2007e42 --- /dev/null +++ b/Utils/Errors.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +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 }; + } +} diff --git a/Utils/OperationResult.cs b/Utils/OperationResult.cs new file mode 100644 index 0000000..e8effbc --- /dev/null +++ b/Utils/OperationResult.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace DDDCommon.Utils +{ + public class OperationResult + { + public bool Success { get; set; } + public Error Error { get; set; } + + public OperationResult(bool success, Error error = null) + { + Success = success; + Error = error; + } + + public static async Task Try(Func func) + { + try + { + await func(); + return new OperationResult(true); + } + catch (Exception e) + { + return new OperationResult(false, + e as Error ?? Errors.InternalError(e.Message)); + } + } + } + + public class OperationResult : OperationResult + { + public T Result { get; set; } + + public OperationResult(bool success, T result, Error error = null) + : base(success, error) + { + Result = result; + } + + public static async Task> Try(Func> func) + { + try + { + return new OperationResult(true, await func(), null); + } + catch (Exception e) + { + return new OperationResult(false, default, + e as Error ?? Errors.InternalError(e.Message)); + } + } + } + + [JsonConverter(typeof(ErrorJsonConverter))] + public class Error : Exception + { + public int Code { get; set; } + public string Text { get; set; } + public string Details { get; set; } + } +}