Initial .NET 8 Web API with EF Core Db-First for Catalogs

Set up multi-project solution with DbFirst API, Application, Domain, and Infrastructure layers. Implemented database-first EF Core for the Catalog entity, including domain, DTOs, repository, service, and controller. Configured AutoMapper, DI, Swagger, and project settings. Added .gitattributes and initial configuration files.
This commit is contained in:
OlgunR
2026-01-12 09:42:20 +01:00
parent ecd00447fd
commit d312230803
23 changed files with 628 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,63 @@
using DbFirst.Application.Catalogs;
using Microsoft.AspNetCore.Mvc;
namespace DbFirst.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class CatalogsController : ControllerBase
{
private readonly ICatalogService _service;
public CatalogsController(ICatalogService service)
{
_service = service;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<CatalogDto>>> GetAll(CancellationToken cancellationToken)
{
var result = await _service.GetAllAsync(cancellationToken);
return Ok(result);
}
[HttpGet("{id:int}")]
public async Task<ActionResult<CatalogDto>> GetById(int id, CancellationToken cancellationToken)
{
var result = await _service.GetByIdAsync(id, cancellationToken);
if (result == null)
{
return NotFound();
}
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<CatalogDto>> Create(CatalogDto dto, CancellationToken cancellationToken)
{
var created = await _service.CreateAsync(dto, cancellationToken);
return CreatedAtAction(nameof(GetById), new { id = created.Guid }, created);
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, CatalogDto dto, CancellationToken cancellationToken)
{
var updated = await _service.UpdateAsync(id, dto, cancellationToken);
if (!updated)
{
return NotFound();
}
return NoContent();
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
var deleted = await _service.DeleteAsync(id, cancellationToken);
if (!deleted)
{
return NotFound();
}
return NoContent();
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbFirst.Application\DbFirst.Application.csproj" />
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
<ProjectReference Include="..\DbFirst.Infrastructure\DbFirst.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@DbFirst.API_HostAddress = http://localhost:5131
GET {{DbFirst.API_HostAddress}}/weatherforecast/
Accept: application/json
###

38
DbFirst.API/Program.cs Normal file
View File

@@ -0,0 +1,38 @@
using AutoMapper;
using DbFirst.Application.Catalogs;
using DbFirst.Domain.Repositories;
using DbFirst.Infrastructure;
using DbFirst.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddAutoMapper(typeof(CatalogProfile).Assembly, typeof(ApplicationDbContext).Assembly);
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
builder.Services.AddScoped<ICatalogService, CatalogService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:27897",
"sslPort": 44349
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5131",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7204;http://localhost:5131",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,12 @@
namespace DbFirst.Application.Catalogs;
public class CatalogDto
{
public int Guid { get; set; }
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,12 @@
using AutoMapper;
using DbFirst.Domain.DomainEntities;
namespace DbFirst.Application.Catalogs;
public class CatalogProfile : Profile
{
public CatalogProfile()
{
CreateMap<Catalog, CatalogDto>().ReverseMap();
}
}

View File

@@ -0,0 +1,47 @@
using AutoMapper;
using DbFirst.Domain.DomainEntities;
using DbFirst.Domain.Repositories;
namespace DbFirst.Application.Catalogs;
public class CatalogService : ICatalogService
{
private readonly ICatalogRepository _repository;
private readonly IMapper _mapper;
public CatalogService(ICatalogRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<CatalogDto>> GetAllAsync(CancellationToken cancellationToken = default)
{
var domainItems = await _repository.GetAllAsync(cancellationToken);
return _mapper.Map<List<CatalogDto>>(domainItems);
}
public async Task<CatalogDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
var domainItem = await _repository.GetByIdAsync(id, cancellationToken);
return domainItem == null ? null : _mapper.Map<CatalogDto>(domainItem);
}
public async Task<CatalogDto> CreateAsync(CatalogDto dto, CancellationToken cancellationToken = default)
{
var domainItem = _mapper.Map<Catalog>(dto);
var created = await _repository.AddAsync(domainItem, cancellationToken);
return _mapper.Map<CatalogDto>(created);
}
public async Task<bool> UpdateAsync(int id, CatalogDto dto, CancellationToken cancellationToken = default)
{
var domainItem = _mapper.Map<Catalog>(dto);
return await _repository.UpdateAsync(id, domainItem, cancellationToken);
}
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
{
return await _repository.DeleteAsync(id, cancellationToken);
}
}

View File

@@ -0,0 +1,10 @@
namespace DbFirst.Application.Catalogs;
public interface ICatalogService
{
Task<List<CatalogDto>> GetAllAsync(CancellationToken cancellationToken = default);
Task<CatalogDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<CatalogDto> CreateAsync(CatalogDto dto, CancellationToken cancellationToken = default);
Task<bool> UpdateAsync(int id, CatalogDto dto, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="DomainEntities\" />
<Folder Include="Repositories\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,12 @@
namespace DbFirst.Domain.DomainEntities;
public class Catalog
{
public int Guid { get; set; }
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
}

View File

@@ -0,0 +1,12 @@
using DbFirst.Domain.DomainEntities;
namespace DbFirst.Domain.Repositories;
public interface ICatalogRepository
{
Task<List<Catalog>> GetAllAsync(CancellationToken cancellationToken = default);
Task<Catalog?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<Catalog> AddAsync(Catalog catalog, CancellationToken cancellationToken = default);
Task<bool> UpdateAsync(int id, Catalog catalog, CancellationToken cancellationToken = default);
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,56 @@
using DbFirst.Infrastructure.ScaffoldEntities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure;
public partial class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<TbmyCatalog> TbmyCatalogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TbmyCatalog>(entity =>
{
entity.HasKey(e => e.Guid);
entity.ToTable("TBMY_CATALOG");
entity.HasIndex(e => e.CatTitle, "UQ_TBMY_CATALOG_TITLE").IsUnique();
entity.Property(e => e.Guid).HasColumnName("GUID");
entity.Property(e => e.AddedWhen)
.HasDefaultValueSql("(getdate())")
.HasColumnType("datetime")
.HasColumnName("ADDED_WHEN");
entity.Property(e => e.AddedWho)
.HasMaxLength(30)
.IsUnicode(false)
.HasDefaultValue("SYSTEM")
.HasColumnName("ADDED_WHO");
entity.Property(e => e.CatString)
.HasMaxLength(900)
.IsUnicode(false)
.HasColumnName("CAT_STRING");
entity.Property(e => e.CatTitle)
.HasMaxLength(100)
.IsUnicode(false)
.HasColumnName("CAT_TITLE");
entity.Property(e => e.ChangedWhen)
.HasColumnType("datetime")
.HasColumnName("CHANGED_WHEN");
entity.Property(e => e.ChangedWho)
.HasMaxLength(30)
.IsUnicode(false)
.HasColumnName("CHANGED_WHO");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.22" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.22">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
using AutoMapper;
using DbFirst.Domain.DomainEntities;
using DbFirst.Infrastructure.ScaffoldEntities;
namespace DbFirst.Infrastructure.Mappings;
public class CatalogInfrastructureProfile : Profile
{
public CatalogInfrastructureProfile()
{
CreateMap<TbmyCatalog, Catalog>().ReverseMap();
}
}

View File

@@ -0,0 +1,66 @@
using AutoMapper;
using DbFirst.Domain.DomainEntities;
using DbFirst.Domain.Repositories;
using DbFirst.Infrastructure.ScaffoldEntities;
using Microsoft.EntityFrameworkCore;
namespace DbFirst.Infrastructure.Repositories;
public class CatalogRepository : ICatalogRepository
{
private readonly ApplicationDbContext _db;
private readonly IMapper _mapper;
public CatalogRepository(ApplicationDbContext db, IMapper mapper)
{
_db = db;
_mapper = mapper;
}
public async Task<List<Catalog>> GetAllAsync(CancellationToken cancellationToken = default)
{
var entities = await _db.TbmyCatalogs.AsNoTracking().ToListAsync(cancellationToken);
return _mapper.Map<List<Catalog>>(entities);
}
public async Task<Catalog?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
var entity = await _db.TbmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.Guid == id, cancellationToken);
return entity == null ? null : _mapper.Map<Catalog>(entity);
}
public async Task<Catalog> AddAsync(Catalog catalog, CancellationToken cancellationToken = default)
{
var entity = _mapper.Map<TbmyCatalog>(catalog);
_db.TbmyCatalogs.Add(entity);
await _db.SaveChangesAsync(cancellationToken);
return _mapper.Map<Catalog>(entity);
}
public async Task<bool> UpdateAsync(int id, Catalog catalog, CancellationToken cancellationToken = default)
{
var entity = await _db.TbmyCatalogs.FirstOrDefaultAsync(x => x.Guid == id, cancellationToken);
if (entity == null)
{
return false;
}
_mapper.Map(catalog, entity);
entity.Guid = id;
await _db.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
{
var entity = await _db.TbmyCatalogs.FirstOrDefaultAsync(x => x.Guid == id, cancellationToken);
if (entity == null)
{
return false;
}
_db.TbmyCatalogs.Remove(entity);
await _db.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,24 @@
namespace DbFirst.Infrastructure.ScaffoldEntities;
public partial class TbmyCatalog
{
public int Guid { get; set; }
public string CatTitle { get; set; } = null!;
public string CatString { get; set; } = null!;
public string AddedWho { get; set; } = null!;
public DateTime AddedWhen { get; set; }
public string? ChangedWho { get; set; }
public DateTime? ChangedWhen { get; set; }
// = null!; ist nur eine Null-Unterdrückung für NonNullable-Referenztypen. Hintergrund: Die Spalten CatTitle, CatString, AddedWho
// sind in der DB als NOT NULL definiert, also generiert der Scaffold string (ohne ?). Damit der Compiler bei aktivierter Nullable-Analyse
// nicht warnt („non-nullable property is uninitialized“), wird ein Dummy-Init auf null! gesetzt. Das ! sagt dem Compiler „ich garantiere,
// dass zur Laufzeit ein Wert gesetzt wird“. Bei string? ChangedWho/DateTime? ChangedWhen sind die Spalten nullable, daher kein null! nötig.
// Bei Value Types wie int/DateTime braucht es ebenfalls kein Init, da sie Standardwerte haben.
}

43
DbFirst.sln Normal file
View File

@@ -0,0 +1,43 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36804.6 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.API", "DbFirst.API\DbFirst.API.csproj", "{01A1D6C0-CEF6-4E90-A23D-B8594B72D8D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Application", "DbFirst.Application\DbFirst.Application.csproj", "{5FDD6C63-CFC3-48AD-9857-4AAA4C97D6D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Infrastructure", "DbFirst.Infrastructure\DbFirst.Infrastructure.csproj", "{16377B21-372D-438D-93F9-EBBA84E7CC73}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbFirst.Domain", "DbFirst.Domain\DbFirst.Domain.csproj", "{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{01A1D6C0-CEF6-4E90-A23D-B8594B72D8D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01A1D6C0-CEF6-4E90-A23D-B8594B72D8D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01A1D6C0-CEF6-4E90-A23D-B8594B72D8D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01A1D6C0-CEF6-4E90-A23D-B8594B72D8D2}.Release|Any CPU.Build.0 = Release|Any CPU
{5FDD6C63-CFC3-48AD-9857-4AAA4C97D6D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5FDD6C63-CFC3-48AD-9857-4AAA4C97D6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FDD6C63-CFC3-48AD-9857-4AAA4C97D6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FDD6C63-CFC3-48AD-9857-4AAA4C97D6D0}.Release|Any CPU.Build.0 = Release|Any CPU
{16377B21-372D-438D-93F9-EBBA84E7CC73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16377B21-372D-438D-93F9-EBBA84E7CC73}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16377B21-372D-438D-93F9-EBBA84E7CC73}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16377B21-372D-438D-93F9-EBBA84E7CC73}.Release|Any CPU.Build.0 = Release|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E989468B-CBF1-49F4-954E-4FFEE7CE5A77}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {17F5E3D8-8F34-4A46-ACEB-DF4BBA46AB0E}
EndGlobalSection
EndGlobal

9
NuGet.config Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<fallbackPackageFolders>
<clear />
</fallbackPackageFolders>
<config>
<add key="globalPackagesFolder" value="%USERPROFILE%\.nuget\packages" />
</config>
</configuration>