diff --git a/Solution-Template.sln b/Solution-Template.sln index 09330aa..e6129a3 100644 --- a/Solution-Template.sln +++ b/Solution-Template.sln @@ -1,21 +1,68 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microservice", "src\Api\Microservice.csproj", "{AC3F0F1B-31CD-47D4-953F-FC970261B9FB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal \ No newline at end of file + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microservice", "src\Api\Microservice.csproj", "{AC3F0F1B-31CD-47D4-953F-FC970261B9FB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infra", "src\Infra\Infra.csproj", "{73439EED-AE10-430D-99E5-2BD7E922BA8E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|x64.Build.0 = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Debug|x86.Build.0 = Debug|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|Any CPU.Build.0 = Release|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|x64.ActiveCfg = Release|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|x64.Build.0 = Release|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|x86.ActiveCfg = Release|Any CPU + {AC3F0F1B-31CD-47D4-953F-FC970261B9FB}.Release|x86.Build.0 = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|x64.ActiveCfg = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|x64.Build.0 = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|x86.ActiveCfg = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Debug|x86.Build.0 = Debug|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|Any CPU.Build.0 = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|x64.ActiveCfg = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|x64.Build.0 = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|x86.ActiveCfg = Release|Any CPU + {73439EED-AE10-430D-99E5-2BD7E922BA8E}.Release|x86.Build.0 = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|x64.ActiveCfg = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|x64.Build.0 = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|x86.ActiveCfg = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Debug|x86.Build.0 = Debug|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|Any CPU.Build.0 = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|x64.ActiveCfg = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|x64.Build.0 = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|x86.ActiveCfg = Release|Any CPU + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {73439EED-AE10-430D-99E5-2BD7E922BA8E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {A664D19C-0BDC-4B5F-AD31-C7BA0217DD22} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection +EndGlobal diff --git a/src/Api/Program.cs b/src/Api/Program.cs index e6d610c..19eec38 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -3,6 +3,10 @@ using Api.Middlewares; using Asp.Versioning; using Asp.Versioning.Builder; +using Domain.Interfaces; +using Infra.Data; +using Infra.Repositories; +using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; using Prometheus; using Scalar.AspNetCore; @@ -13,6 +17,11 @@ builder.Services.AddControllers(); +builder.Services + .AddDbContext(opts => opts.UseInMemoryDatabase("DemoDb")) + .AddScoped(typeof(IRepository<>), typeof(Repository<>)) + .AddScoped(); + builder.Services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1, 0); diff --git a/src/Domain/Contracts/.gitkeep b/src/Domain/Contracts/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj index ddfae90..99f72e9 100644 --- a/src/Domain/Domain.csproj +++ b/src/Domain/Domain.csproj @@ -2,7 +2,7 @@ net9.0 Domain - disable + enable enable diff --git a/src/Domain/Entities/BaseEntity.cs b/src/Domain/Entities/BaseEntity.cs new file mode 100644 index 0000000..1227160 --- /dev/null +++ b/src/Domain/Entities/BaseEntity.cs @@ -0,0 +1,8 @@ +namespace Domain.Entities; + +public abstract class BaseEntity +{ + public TKey Id { get; set; } = default!; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/Domain/Entities/SampleEntity.cs b/src/Domain/Entities/SampleEntity.cs new file mode 100644 index 0000000..a438ad6 --- /dev/null +++ b/src/Domain/Entities/SampleEntity.cs @@ -0,0 +1,6 @@ +namespace Domain.Entities; + +public class SampleEntity : BaseEntity +{ + public string Name { get; set; } = default!; +} diff --git a/src/Domain/Interfaces/IRepository.cs b/src/Domain/Interfaces/IRepository.cs new file mode 100644 index 0000000..29d76b4 --- /dev/null +++ b/src/Domain/Interfaces/IRepository.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; + +namespace Domain.Interfaces; + +public interface IRepository : IAsyncDisposable where TEntity : class +{ + IQueryable Query(bool asNoTracking = true); + + Task GetByIdAsync(TKey id, CancellationToken cancellationToken = default); + + Task> ListAsync(Expression>? predicate = null, CancellationToken cancellationToken = default); + + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + + Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); + + Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default); + + Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default); + + Task UpdateAsync(Expression> predicate, object updateDefinition, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Domain/Interfaces/IUnitOfWork.cs b/src/Domain/Interfaces/IUnitOfWork.cs new file mode 100644 index 0000000..96d8ea8 --- /dev/null +++ b/src/Domain/Interfaces/IUnitOfWork.cs @@ -0,0 +1,9 @@ +namespace Domain.Interfaces; + +public interface IUnitOfWork : IAsyncDisposable +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); + + // TODO: implement transaction management + // Task BeginTransactionAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Infra/Data/.gitkeep b/src/Infra/Data/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Infra/Data/DataContext.cs b/src/Infra/Data/DataContext.cs new file mode 100644 index 0000000..eced47b --- /dev/null +++ b/src/Infra/Data/DataContext.cs @@ -0,0 +1,10 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infra.Data; +public class DataContext : DbContext +{ + public DataContext(DbContextOptions opts) : base(opts) { } + + public virtual DbSet Products => Set(); +} \ No newline at end of file diff --git a/src/Infra/Infra.csproj b/src/Infra/Infra.csproj index 7af02ff..9806bce 100644 --- a/src/Infra/Infra.csproj +++ b/src/Infra/Infra.csproj @@ -2,7 +2,16 @@ net9.0 Infra - disable + enable enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + \ No newline at end of file diff --git a/src/Infra/Repositories/Repository.cs b/src/Infra/Repositories/Repository.cs new file mode 100644 index 0000000..bc7bf59 --- /dev/null +++ b/src/Infra/Repositories/Repository.cs @@ -0,0 +1,78 @@ +using Infra.Data; +using Domain.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Query; + +namespace Infra.Repositories; + +public class Repository : IRepository where TEntity : class +{ + private readonly DataContext _dataContext; + private readonly DbSet _dbSet; + private int _disposed; + + public Repository(DataContext dataContext) + { + _dataContext = dataContext; + _dbSet = dataContext.Set(); + } + + public IQueryable Query(bool asNoTracking = true) => (asNoTracking ? _dbSet.AsNoTracking() : _dbSet).AsQueryable(); + + public Task GetByIdAsync(TKey id, CancellationToken cancellationToken = default) + { + return _dbSet.FindAsync(new object?[] { id }!, cancellationToken).AsTask(); + } + + public Task> ListAsync(Expression>? predicate = null, CancellationToken cancellationToken = default) + { + IQueryable query = _dbSet; + if (predicate is not null) query = query.Where(predicate); + return query.ToListAsync(cancellationToken); + } + + public async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await _dbSet.AddAsync(entity, cancellationToken); + + return entity; + } + + public Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + _dbSet.Update(entity); + + return Task.CompletedTask; + } + + public Task DeleteAsync(TEntity entity, CancellationToken cancellationToken = default) + { + _dbSet.Remove(entity); + + return Task.CompletedTask; + } + + public Task DeleteAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + return _dbSet.Where(predicate).ExecuteDeleteAsync(cancellationToken); + } + + public Task UpdateAsync(Expression> predicate, object updateDefinition, CancellationToken cancellationToken = default) + { + if (updateDefinition is not Expression, SetPropertyCalls>> expression) + { + throw new InvalidOperationException($"Invalid update definition for type '{typeof(TEntity).Name}'. Expected an expression of type Expression, SetPropertyCalls<{typeof(TEntity).Name}>>>."); + } + + return _dbSet.Where(predicate).ExecuteUpdateAsync(expression, cancellationToken); + } + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + await _dataContext.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/src/Infra/Repositories/UnitOfWork.cs b/src/Infra/Repositories/UnitOfWork.cs new file mode 100644 index 0000000..e422049 --- /dev/null +++ b/src/Infra/Repositories/UnitOfWork.cs @@ -0,0 +1,22 @@ +using Infra.Data; +using Domain.Interfaces; + +namespace Infra.Repositories; + +public class UnitOfWork : IUnitOfWork +{ + private readonly DataContext _dataContext; + private int _disposed; + + public UnitOfWork(DataContext dataContext) => _dataContext = dataContext; + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => _dataContext.SaveChangesAsync(cancellationToken); + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + await _dataContext.DisposeAsync(); + } + } +} \ No newline at end of file