[C# .NET] .NET API 베이직. (Dapper, EntityFramework)

박제현·2024년 6월 10일
0

.NET

목록 보기
5/7

시작.


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 명령어를 통해서 추가해주자.

appsettings.json

{
  "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 을 선언해준다.

launchSettings.json


프로젝트 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"
      }
    }
  }
}

새로운 프로젝트 생성시 profileshttp, https 의 URL 포트번호가 랜덤으로 생성되므로, 익숙한 5000번 포트와 5001번 포트로 변경해준다.

Program.cs

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();

Controller

builder.Services.AddControllers(); 를 통해서 우리가 생성한 컨트롤러들을 연결 시켜준다.
app.MapControllers(); 을 통해서 실행된 웹 애플리케이션에 컨트롤러를 연결 시켜준다.

Cors

builder.Services.AddCors() 를 통해서 CORS 정책을 설정해준다.
ArrowFunction 으로 내부에서 CorsOption 을 사용하고, options.AddPolicy() 을 통해서 새로운 정책을 설정할 수 있다.
이 프로젝트에서는 DevCorsProductCors 로 정책을 분리하여 설정하였다.
이후 app.UseCors() 를 통해서 사용자가 선언한 CORS 정책을 사용할 수 있다.

Models

컨트롤러와 DB 사이에 통신에 필요한 모델들을 생성한다.

User.cs

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 ??= "";
    }
}

UserJobInfo.cs

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 ??= "";
     }
}

UserSalary.cs

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() { }
}

위와 같이 리팩토링하여 캡슐화를 유지하고, 필드에 대한 접근 및 수정 로직을 제어한다.

Data (Dapper, EntityFramework)

Dapper

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 문을 직접 전송하여 결과를 반환하는 방식으로, IConfigurationIDbConnection 정보만 초기화 해주면 사용 가능하다.

EntityFramework

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 를 통해서, 스키마를 설정하고, 각 엔티티에 기본키를 설정해준다.

Controller

[ApiController][Route("[controller]")] 어노테이션을 통해 컨트롤러임을 명시하고, CotrollerBase를 상속 받아 HttpResult 를 반환할 수 있도록 한다.

DapperController

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 한다.

EntityFramework Controller

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

AutoMapper 를 사용하기 위해서 생성자에 IMapper 객체를 초기화 시켜주는 로직이 들어있다.

_mapper = new Mapper(new MapperConfiguration(config =>
        {
            config.CreateMap<UserToAddDto, User>();
        }));

UserToAddDto 객체를 User 객체로 자동으로 매핑해준다.

결과

정상적으로 값을 반환하는 모습.

PS.

캡슐화와 프로퍼티를 통한 리팩토링 한 결과, 순환 참조 에러가 발생했다.
아직.. C#에 대해 깊이 알지 못하기 때문에, 왜 이런건지를 모르겠다.
강의 영상에서도 굳이 public 으로 필드를 생성한 이유가 있는 것인가?

profile
닷넷 새싹

0개의 댓글

관련 채용 정보