dotnet new webapi -n "프로젝트 명"
으로 새로운 .NET API 프로젝트를 생성한다.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>
초기 설정은 net8.0 버전으로 맞춰져 있다.
<ItemGroup>
<PackageReference Include="automapper" Version="13.0.1" />
<PackageReference Include="dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
<PackageReference Include="microsoft.entityframeworkcore" Version="8.0.6" />
<PackageReference Include="microsoft.entityframeworkcore.relational" Version="8.0.6" />
<PackageReference Include="microsoft.entityframeworkcore.sqlserver" Version="8.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
프로젝트에 필요한 패키지들이다.
dotnet add package
명령어를 통해서 추가해주자.
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=DotNetCourseDatabase;Trusted_Connection=false;TrustServerCertificate=true;User Id=sa;Password=SQLConnect1"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
DB 연결에 필요한 ConnectionString
을 선언해준다.
프로젝트 Properties
디렉토리 내부에 존재한다.
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34101",
"sslPort": 44330
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5000;https://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
새로운 프로젝트 생성시 profiles
의 http
, https
의 URL 포트번호가 랜덤으로 생성되므로, 익숙한 5000번 포트와 5001번 포트로 변경해준다.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddCors(options =>
{
options.AddPolicy("DevCors",
policyBuilder =>
{
policyBuilder.WithOrigins("http://localhost:4200", "http://localhost:3000", "http://localhost:8000")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
options.AddPolicy("ProductCors",
policyBuilder =>
{
policyBuilder.WithOrigins("https://myProductionSite.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseCors("DevCors");
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseCors("ProductCors");
app.UseHttpsRedirection();
}
app.MapControllers();
app.Run();
builder.Services.AddControllers();
를 통해서 우리가 생성한 컨트롤러들을 연결 시켜준다.
app.MapControllers();
을 통해서 실행된 웹 애플리케이션에 컨트롤러를 연결 시켜준다.
builder.Services.AddCors()
를 통해서 CORS 정책을 설정해준다.
ArrowFunction 으로 내부에서 CorsOption
을 사용하고, options.AddPolicy()
을 통해서 새로운 정책을 설정할 수 있다.
이 프로젝트에서는 DevCors
와 ProductCors
로 정책을 분리하여 설정하였다.
이후 app.UseCors()
를 통해서 사용자가 선언한 CORS 정책을 사용할 수 있다.
컨트롤러와 DB 사이에 통신에 필요한 모델들을 생성한다.
namespace netAPI.Models;
public class User
{
public int UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Gender { get; set; }
public bool Active { get; set; }
public User()
{
if (FirstName == null) FirstName = "";
LastName ??= "";
Email ??= "";
Gender ??= "";
}
}
namespace netAPI.Models;
public class UserJobInfo
{
public int UserId { get; set; }
public string JobTitle { get; set; }
public string Department { get; set; }
public UserJobInfo()
{
JobTitle ??= "";
Department ??= "";
}
}
namespace netAPI.Models;
public class UserSalary
{
public int UserId { get; set; }
public decimal Salary { get; set; }
public decimal AvgSalary { get; set; }
}
??=
null
병합 연산자로??
는 왼쪽 피연산자가null
이 아닌 경우 자신의 값을 반환하고, 그렇지 않으면 오른쪽 피연산자 값을 반환한다.
해당 모델들의 문제점이 있는데 무엇일까??
바로 필드 값을 public
으로 선언했다는 점이다.
그리고 이 필드에 get
, set
프로퍼티를 사용하는 것은 캡슐화를 사용하지 못한다.
따라서 아래와 같이 수정하는게 바람직해 보인다.
namespace netAPI.Models;
public class User
{
private int _UserId;
private string _FirstName;
private string _LastName;
private string _Email;
private string _Gender;
private bool _Active;
public int UserId
{
get { return _UserId; }
set { _UserId = value; }
}
public string FirstName
{
get { return _FirstName; }
set { _FirstName = value ?? ""; }
}
public string LastName
{
get { return _LastName; }
set { _LastName = value ?? ""; }
}
public string Email
{
get { return _Email; }
set { _Email = value ?? ""; }
}
public string Gender
{
get { return _Gender; }
set { _Gender = value ?? ""; }
}
public bool Active
{
get { return Active; }
set { _Active = value; }
}
public User() { }
}
위와 같이 리팩토링하여 캡슐화를 유지하고, 필드에 대한 접근 및 수정 로직을 제어한다.
using System.Data;
using Dapper;
using Microsoft.Data.SqlClient;
namespace netAPI.Data;
public class DataContextDapper
{
private readonly IConfiguration _configuration;
private readonly IDbConnection _dbConnection;
public DataContextDapper(IConfiguration configuration)
{
_configuration = configuration;
_dbConnection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection"));
}
public IEnumerable<T> LoadData<T>(string sql)
{
return _dbConnection.Query<T>(sql);
}
public T LoadDataSingle<T>(string sql)
{
return _dbConnection.QuerySingle<T>(sql);
}
public bool ExecuteSql(string sql)
{
return _dbConnection.Execute(sql) > 0;
}
public int ExecuteSqlWithRowCounts(string sql)
{
return _dbConnection.Execute(sql);
}
}
Dapper 를 이용한 객체와 데이터베이스 매핑의 장점
- 단순하며, 성능에 초점을 맞추고 있다.
- 개발자가 로우 쿼리문을 작성하고 결과를 객체에 직접 매핑할 수 있다.
- 추상화를 최소화하고 쿼리를 직접 실행하여 더 빠른 데이터 엑세스를 제공한다.
- 성능이 중요한 프로젝트나 SQL 쿼리에 대한 더 많은 제어를 선호하는 경우에 적합하다.
Dapper 는 개발자의 SQL 문을 직접 전송하여 결과를 반환하는 방식으로, IConfiguration
과 IDbConnection
정보만 초기화 해주면 사용 가능하다.
using Microsoft.EntityFrameworkCore;
using netAPI.Models;
namespace netAPI.Data;
public class DataContextEF : DbContext
{
private readonly IConfiguration _configuration;
public DataContextEF(IConfiguration configuration)
{
_configuration = configuration;
}
public virtual DbSet<User> Users { get; set; }
public virtual DbSet<UserSalary> UserSalary { get; set; }
public virtual DbSet<UserJobInfo> UserJobInfo { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseSqlServer(_configuration.GetConnectionString("DefaultConnection"),
optionsBuilder => optionsBuilder.EnableRetryOnFailure());
}
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("TutorialAppSchema");
modelBuilder.Entity<User>()
.ToTable("Users", "TutorialAppSchema")
.HasKey(user => user.UserId);
modelBuilder.Entity<UserSalary>().HasKey(user => user.UserId);
modelBuilder.Entity<UserJobInfo>().HasKey(user => user.UserId);
base.OnModelCreating(modelBuilder);
}
}
Entity Framework 를 이용한 객체와 데이터베이스 매핑 장점
- 모든 기능을 가지고 있는 ORM 프레임워크다.
- 데이터베이스에 대해 더 높은 수준의 추상화를 제공한다.
- Code-First, Database-First 및 Model-First 와 같은 접근 방식을 지원한다.
- 변경 내용 추적 및 쿼리 번역을 포함하여 데이터 액세스의 여러 측면을 자동화한다.
- 고수준의 추상화와 더 많은 기능을 제공하는 ROM 솔루션을 선호하는 경우에 적합하다.
Entity Framework 는 Dapper 에 비해 훨씬 더 다양한 자동화를 제공한다.
초기 설정시 IConfiguration
을 초기화 해준다.
이후 OnConfiguring
메소드를 오버라이드 하여 _configuration
에 등록된 DefaultConnection
으로 db 를 연결해준다.
OnModelCreating
메소드는 이 프로젝트 상에서 필요한 설정으로, 실제 데이터베이스의 Table
명은 Users
로 작성했으나, C# 프로젝트에서 User.cs
로 모델을 생성했기 때문에 이를 매핑해줄 필요가 있다.
ModelBuilder
를 통해서, 스키마를 설정하고, 각 엔티티에 기본키를 설정해준다.
[ApiController]
와 [Route("[controller]")]
어노테이션을 통해 컨트롤러임을 명시하고, CotrollerBase를 상속 받아 HttpResult
를 반환할 수 있도록 한다.
using System.Collections;
using Microsoft.AspNetCore.Mvc;
using netAPI.Data;
using netAPI.DTOs;
using netAPI.Models;
namespace netAPI.Controllers;
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
private DataContextDapper _dapper;
public UserController(IConfiguration configuration)
{
_dapper = new DataContextDapper(configuration);
}
[HttpGet("TestConnection")]
public DateTime TestConnection()
{
return _dapper.LoadDataSingle<DateTime>("SELECT GETDATE();");
}
[HttpGet("TestExecute")]
public bool TestExecute()
{
return _dapper.ExecuteSql("SELECT GETDATE();");
}
[HttpGet("GetUsers")]
public IEnumerable<User> GetUsers()
{
return _dapper.LoadData<User>("SELECT * FROM TutorialAppSchema.Users;");
}
[HttpGet("GetSingleUser/{userId}")]
public User GetSingleUser(int userId)
{
return _dapper.LoadDataSingle<User>("SELECT * FROM TutorialAppSchema.Users WHERE UserId = " + userId);
}
[HttpPut]
public IActionResult EditUser(User user)
{
string sql = $"""
UPDATE TutorialAppSchema.Users
SET [FirstName] = '{user.FirstName}',
[LastName] = '{user.LastName}',
[Email] = '{user.Email}',
[Gender] = '{user.Gender}',
[Active] = '{user.Active}'
WHERE UserId = {user.UserId};
""";
Console.WriteLine(sql);
if (_dapper.ExecuteSql(sql)) return Ok("Edit Succeed.");
throw new Exception("Failed to Update User.");
}
[HttpPost]
public IActionResult AddUser(UserToAddDto userToAdd)
{
string sql = $"""
INSERT INTO TutorialAppSchema.Users(
[FirstName],
[LastName],
[Email],
[Gender],
[Active]
) VALUES (
'{userToAdd.FirstName}',
'{userToAdd.LastName}',
'{userToAdd.Email}',
'{userToAdd.Gender}',
'{userToAdd.Active}'
);
""";
Console.WriteLine(sql);
if (_dapper.ExecuteSql(sql)) return Ok("Add user Succeed.");
throw new Exception("Failed to Add User.");
}
[HttpDelete("DeleteUser/{userId}")]
public IActionResult DeleteUser(int userId)
{
string sql = $"""
DELETE
FROM TutorialAppSchema.Users
WHERE UserId = {userId};
""";
Console.WriteLine(sql);
if (_dapper.ExecuteSql(sql)) return Ok("Delete User Succeed.");
throw new Exception("Failed to Delete User.");
}
}
Dapper Controller 는 SQL 쿼리문을 직접 작성해서
Execute
한다.
using System.Collections;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using netAPI.Data;
using netAPI.DTOs;
using netAPI.Models;
namespace netAPI.Controllers;
[ApiController]
[Route("[controller]")]
public class UserEFController : ControllerBase
{
private DataContextEF _entityFramework;
private IMapper _mapper;
public UserEFController(IConfiguration configuration)
{
_entityFramework = new DataContextEF(configuration);
_mapper = new Mapper(new MapperConfiguration(config =>
{
config.CreateMap<UserToAddDto, User>();
}));
}
[HttpGet("GetUsers")]
public IEnumerable<User> GetUsers()
{
return _entityFramework.Users.ToList<User>();
}
[HttpGet("GetSingleUser/{userId}")]
public User GetSingleUser(int userId)
{
User? user = _entityFramework.Users.FirstOrDefault(user => user.UserId == userId);
if (user != null) return user;
throw new Exception("Failed to Get User.");
}
[HttpPut]
public IActionResult EditUser(User user)
{
User? userDb = _entityFramework.Users.FirstOrDefault(u => u.UserId == user.UserId);
if (userDb != null)
{
userDb.FirstName = user.FirstName;
userDb.LastName = user.LastName;
userDb.Email = user.Email;
userDb.Gender = user.Gender;
userDb.Active = user.Active;
if (_entityFramework.SaveChanges() > 0)
{
return Ok();
}
}
throw new Exception("Failed to Update User.");
}
[HttpPost]
public IActionResult AddUser(UserToAddDto userToAdd)
{
/*
User? userDb = new User();
userDb.FirstName = userToAdd.FirstName;
userDb.LastName = userToAdd.LastName;
userDb.Email = userToAdd.Email;
userDb.Gender = userToAdd.Gender;
userDb.Active = userToAdd.Active;
*/
User userDb = _mapper.Map<User>(userToAdd);
_entityFramework.Add(userDb);
if (_entityFramework.SaveChanges() > 0) return Ok("Add User Succeed.");
throw new Exception("Failed to Add User.");
}
[HttpDelete("DeleteUser/{userId}")]
public IActionResult DeleteUser(int userId)
{
User? userDb = _entityFramework.Users.FirstOrDefault(user => user.UserId == userId);
if (userDb != null)
{
_entityFramework.Users.Remove(userDb);
if (_entityFramework.SaveChanges() > 0) return Ok("Delete User Succeed.");
throw new Exception("Failed to Delete User.");
}
throw new Exception("Failed to Delete User.");
}
}
Entity Framework 의 경우 제공하는 메소드를 통해서 쉽게 db 통신이 가능하다.
AutoMapper 를 사용하기 위해서 생성자에 IMapper
객체를 초기화 시켜주는 로직이 들어있다.
_mapper = new Mapper(new MapperConfiguration(config =>
{
config.CreateMap<UserToAddDto, User>();
}));
UserToAddDto
객체를 User
객체로 자동으로 매핑해준다.
정상적으로 값을 반환하는 모습.
캡슐화와 프로퍼티를 통한 리팩토링 한 결과, 순환 참조 에러가 발생했다.
아직.. C#에 대해 깊이 알지 못하기 때문에, 왜 이런건지를 모르겠다.
강의 영상에서도 굳이 public 으로 필드를 생성한 이유가 있는 것인가?