diff --git a/.vs/MyFirstBlog/DesignTimeBuild/.dtbcache.v2 b/.vs/MyFirstBlog/DesignTimeBuild/.dtbcache.v2 index e3a42a61..bf866b17 100644 Binary files a/.vs/MyFirstBlog/DesignTimeBuild/.dtbcache.v2 and b/.vs/MyFirstBlog/DesignTimeBuild/.dtbcache.v2 differ diff --git a/.vs/MyFirstBlog/FileContentIndex/1451f00c-8a3c-4b67-9988-24ae06d88ff9.vsidx b/.vs/MyFirstBlog/FileContentIndex/1451f00c-8a3c-4b67-9988-24ae06d88ff9.vsidx deleted file mode 100644 index ae1b509b..00000000 Binary files a/.vs/MyFirstBlog/FileContentIndex/1451f00c-8a3c-4b67-9988-24ae06d88ff9.vsidx and /dev/null differ diff --git a/.vs/MyFirstBlog/FileContentIndex/3671b2ea-91c1-4fa3-8590-3e73598d97ea.vsidx b/.vs/MyFirstBlog/FileContentIndex/3671b2ea-91c1-4fa3-8590-3e73598d97ea.vsidx deleted file mode 100644 index 78b55c80..00000000 Binary files a/.vs/MyFirstBlog/FileContentIndex/3671b2ea-91c1-4fa3-8590-3e73598d97ea.vsidx and /dev/null differ diff --git a/.vs/MyFirstBlog/FileContentIndex/6bf9657e-a70c-444b-a131-f31ad350e9a7.vsidx b/.vs/MyFirstBlog/FileContentIndex/6bf9657e-a70c-444b-a131-f31ad350e9a7.vsidx new file mode 100644 index 00000000..59405e99 Binary files /dev/null and b/.vs/MyFirstBlog/FileContentIndex/6bf9657e-a70c-444b-a131-f31ad350e9a7.vsidx differ diff --git a/.vs/MyFirstBlog/FileContentIndex/bea523cd-fe70-40bf-a1b6-2697c5ede561.vsidx b/.vs/MyFirstBlog/FileContentIndex/bea523cd-fe70-40bf-a1b6-2697c5ede561.vsidx new file mode 100644 index 00000000..2b00cd9e Binary files /dev/null and b/.vs/MyFirstBlog/FileContentIndex/bea523cd-fe70-40bf-a1b6-2697c5ede561.vsidx differ diff --git a/.vs/MyFirstBlog/v17/.suo b/.vs/MyFirstBlog/v17/.suo index 74e1a08b..cbb91ce9 100644 Binary files a/.vs/MyFirstBlog/v17/.suo and b/.vs/MyFirstBlog/v17/.suo differ diff --git a/.vs/MyFirstBlogBackendd/v17/.wsuo b/.vs/MyFirstBlogBackendd/v17/.wsuo new file mode 100644 index 00000000..7f24a4af Binary files /dev/null and b/.vs/MyFirstBlogBackendd/v17/.wsuo differ diff --git a/MyFirstBlog/Controllers/PostsController.cs b/MyFirstBlog/Controllers/PostsController.cs index 8fa6bf2c..f8c6b36c 100644 --- a/MyFirstBlog/Controllers/PostsController.cs +++ b/MyFirstBlog/Controllers/PostsController.cs @@ -1,34 +1,52 @@ -namespace MyFirstBlog.Controllers; - using Microsoft.AspNetCore.Mvc; -using MyFirstBlog.Dtos; using MyFirstBlog.Services; +using MyFirstBlog.Dtos; +using System.Collections.Generic; + +namespace MyFirstBlog.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class PostsController : ControllerBase + { + private readonly PostService _postService; + + public PostsController(PostService postService) + { + _postService = postService; + } -[ApiController] -[Route("posts")] + // GET: api/posts + [HttpGet] + public ActionResult> GetPosts() + { + var posts = _postService.GetPosts(); + return Ok(posts); + } -public class PostsController : ControllerBase { - private IPostService _postService; + // GET: api/posts/{slug} + [HttpGet("{slug}")] + public ActionResult GetPost(string slug) + { + var post = _postService.GetPost(slug); + if (post == null) + return NotFound(); - public PostsController(IPostService postService) { - _postService = postService; - } + return Ok(post); + } - // Get /posts - [HttpGet] - public IEnumerable GetPosts() { - return _postService.GetPosts(); - } + // POST: api/posts/new + [HttpPost("new")] + public ActionResult AddPost([FromBody] CreatePostDto createPostDto) + { + if (string.IsNullOrWhiteSpace(createPostDto.Title)) + { + return BadRequest(new { errors = new[] { "Title cannot be blank" } }); + } - // Get /posts/:slug - [HttpGet("{slug}")] - public ActionResult GetPost(string slug) { - var post = _postService.GetPost(slug); + var post = _postService.CreatePost(createPostDto); - if (post is null) { - return NotFound(); + return CreatedAtAction(nameof(GetPost), new { slug = post.Slug }, post); } - - return post; } -} +} \ No newline at end of file diff --git a/MyFirstBlog/Dtos/PostDto.cs b/MyFirstBlog/Dtos/PostDto.cs index 12ca4818..535404d5 100644 --- a/MyFirstBlog/Dtos/PostDto.cs +++ b/MyFirstBlog/Dtos/PostDto.cs @@ -1,9 +1,26 @@ -namespace MyFirstBlog.Dtos; - -public record PostDto { - public Guid Id { get; init; } - public string Title { get; init; } = default!; - public string Slug { get; init; } = default!; - public string Body { get; init; } = default!; - public DateTime CreatedDate { get; init; } +using System; +using System.ComponentModel.DataAnnotations; + +namespace MyFirstBlog.Dtos +{ + public class PostDto + { + public Guid Id { get; set; } + public string Title { get; set; } + public string Slug { get; set; } + public string Body { get; set; } + + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] + public DateTime CreatedDate { get; set; } + } + + public class CreatePostDto + { + public string Title { get; set; } + public string Slug { get; set; } + public string Body { get; set; } + + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd HH:mm:ss}")] + public DateTime CreatedDate { get; set; } + } } diff --git a/MyFirstBlog/Entities/Post.cs b/MyFirstBlog/Entities/Post.cs index 9347e859..17410442 100644 --- a/MyFirstBlog/Entities/Post.cs +++ b/MyFirstBlog/Entities/Post.cs @@ -1,8 +1,11 @@ -namespace MyFirstBlog.Entities; -public record Post { - public Guid Id { get; init; } - public string Title { get; init; } = default!; - public string Slug { get; init; } = default!; - public string Body { get; init; } = default!; - public DateTime CreatedDate { get; init; } +namespace MyFirstBlog.Entities +{ + public class Post + { + public Guid Id { get; set; } + public string Title { get; set; } + public string Slug { get; set; } + public string Body { get; set; } + public DateTime CreatedDate { get; set; } // Updated to CreatedDate + } } diff --git a/MyFirstBlog/Extensions.cs b/MyFirstBlog/Extensions.cs index a5326018..f3865f08 100644 --- a/MyFirstBlog/Extensions.cs +++ b/MyFirstBlog/Extensions.cs @@ -1,18 +1,15 @@ using MyFirstBlog.Dtos; -using MyFirstBlog.Entities; +using System; -namespace MyFirstBlog { - public static class Extensions { - public static PostDto AsDto(this Post post) { - return new PostDto - { - Id = post.Id, - Title = post.Title, - Slug = post.Slug, - Body = post.Body, - CreatedDate = post.CreatedDate - }; - +namespace MyFirstBlog.Extensions +{ + public static class PostExtensions + { + public static void UpdatePost(this PostDto postDto, string title, string body, DateTime createDate) + { + postDto.Title = title; + postDto.Body = body; + postDto.CreatedDate = createDate; // Correct usage of CreateDate } } -}; +} diff --git a/MyFirstBlog/Helpers/ConnectionHelper.cs b/MyFirstBlog/Helpers/ConnectionHelper.cs index 5e6671a6..f985785f 100644 --- a/MyFirstBlog/Helpers/ConnectionHelper.cs +++ b/MyFirstBlog/Helpers/ConnectionHelper.cs @@ -1,31 +1,32 @@ -namespace MyFirstBlog.Helpers; - using Npgsql; +using Microsoft.Extensions.Configuration; -public static class ConnectionHelper +namespace MyFirstBlog.Helpers { - public static string GetConnectionString(IConfiguration configuration) + public static class ConnectionHelper { - var connectionString = configuration.GetConnectionString("DefaultConnection"); - var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL"); - return string.IsNullOrEmpty(databaseUrl) ? connectionString : BuildConnectionString(databaseUrl); - } + public static string GetConnectionString(IConfiguration configuration) + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); + var databaseUrl = Environment.GetEnvironmentVariable("DATABASE_URL"); + return string.IsNullOrEmpty(databaseUrl) ? connectionString : BuildConnectionString(databaseUrl); + } - //build the connection string from the environment. i.e. Heroku or Railway - private static string BuildConnectionString(string databaseUrl) - { - var databaseUri = new Uri(databaseUrl); - var userInfo = databaseUri.UserInfo.Split(':'); - var builder = new NpgsqlConnectionStringBuilder + private static string BuildConnectionString(string databaseUrl) { - Host = databaseUri.Host, - Port = databaseUri.Port, - Username = userInfo[0], - Password = userInfo[1], - Database = databaseUri.LocalPath.TrimStart('/'), - SslMode = SslMode.Require, - TrustServerCertificate = true - }; - return builder.ToString(); - } -} \ No newline at end of file + var databaseUri = new Uri(databaseUrl); + var userInfo = databaseUri.UserInfo.Split(':'); + var builder = new NpgsqlConnectionStringBuilder + { + Host = databaseUri.Host, + Port = databaseUri.Port, + Username = userInfo[0], + Password = userInfo[1], + Database = databaseUri.LocalPath.TrimStart('/'), + SslMode = SslMode.Require, + TrustServerCertificate = true + }; + return builder.ToString(); + } + } +} diff --git a/MyFirstBlog/Helpers/DataContext.cs b/MyFirstBlog/Helpers/DataContext.cs index 60ad2bfc..4ef28ad5 100644 --- a/MyFirstBlog/Helpers/DataContext.cs +++ b/MyFirstBlog/Helpers/DataContext.cs @@ -1,22 +1,23 @@ -namespace MyFirstBlog.Helpers; - -using Microsoft.EntityFrameworkCore; -using MyFirstBlog.Entities; - - -public class DataContext : DbContext +namespace MyFirstBlog.Helpers { - protected readonly IConfiguration Configuration; + using Microsoft.EntityFrameworkCore; + using MyFirstBlog.Entities; + using Microsoft.Extensions.Configuration; - public DataContext(IConfiguration configuration) + public class DataContext : DbContext { - Configuration = configuration; - } + protected readonly IConfiguration Configuration; - protected override void OnConfiguring(DbContextOptionsBuilder options) - { - options.UseNpgsql(ConnectionHelper.GetConnectionString(Configuration)); - } + public DataContext(IConfiguration configuration) + { + Configuration = configuration; + } - public DbSet Posts { get; set; } -} \ No newline at end of file + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + options.UseNpgsql(ConnectionHelper.GetConnectionString(Configuration)); + } + + public DbSet Posts { get; set; } + } +} diff --git a/MyFirstBlog/Helpers/DatabaseHelperBasecs.cs b/MyFirstBlog/Helpers/DatabaseHelperBasecs.cs new file mode 100644 index 00000000..b874b3a2 --- /dev/null +++ b/MyFirstBlog/Helpers/DatabaseHelperBasecs.cs @@ -0,0 +1,16 @@ +namespace MyFirstBlog.Helpers +{ + public static class DatabaseHelperBase + { + + public static async Task ManageMigrationsAsync(IServiceProvider svcProvider) + { + //Service: An instance of db context + var dbContextSvc = svcProvider.GetRequiredService(); + + //Migration: This is the programmatic equivalent to Update-Database + + + } + } +} \ No newline at end of file diff --git a/MyFirstBlog/MyFirstBlog.csproj b/MyFirstBlog/MyFirstBlog.csproj index 04ccc9c1..23089edd 100644 --- a/MyFirstBlog/MyFirstBlog.csproj +++ b/MyFirstBlog/MyFirstBlog.csproj @@ -9,6 +9,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MyFirstBlog/Program.cs b/MyFirstBlog/Program.cs index 4042b610..a550bff7 100644 --- a/MyFirstBlog/Program.cs +++ b/MyFirstBlog/Program.cs @@ -1,7 +1,8 @@ using MyFirstBlog.Helpers; using MyFirstBlog.Services; +using Microsoft.EntityFrameworkCore; -var MyAllowLocalhostOrigins = "_myAllowLocalhostOrigins"; +var MyAllowLocalhostOrigins = "_myAllowLocalhostOrigins"; var builder = WebApplication.CreateBuilder(args); @@ -10,10 +11,11 @@ // Add services to the container. -services.AddDbContext(); +services.AddDbContext(options => + options.UseNpgsql(ConnectionHelper.GetConnectionString(builder.Configuration))); services.AddCors(policyBuilder => { - policyBuilder.AddPolicy( MyAllowLocalhostOrigins, + policyBuilder.AddPolicy(MyAllowLocalhostOrigins, policy => { policy.WithOrigins("http://localhost:3000").AllowAnyHeader().AllowAnyMethod(); }); @@ -24,7 +26,7 @@ services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); -services.AddScoped(); +services.AddScoped(); // Register PostService directly var app = builder.Build(); @@ -49,4 +51,4 @@ app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/MyFirstBlog/Services/PostService.cs b/MyFirstBlog/Services/PostService.cs index 6bac099f..98b424fb 100644 --- a/MyFirstBlog/Services/PostService.cs +++ b/MyFirstBlog/Services/PostService.cs @@ -1,37 +1,83 @@ -namespace MyFirstBlog.Services; - -using MyFirstBlog.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using Dapper; +using Npgsql; using MyFirstBlog.Entities; -using System.Text.RegularExpressions; using MyFirstBlog.Dtos; +using Microsoft.Extensions.Configuration; +using MyFirstBlog.Helpers; -public interface IPostService +namespace MyFirstBlog.Services { - IEnumerable GetPosts(); - PostDto GetPost(String slug); -} + public class PostService + { + private readonly string _connectionString; -public class PostService : IPostService -{ - private DataContext _context; + public PostService(IConfiguration configuration) + { + _connectionString = ConnectionHelper.GetConnectionString(configuration); + } - public PostService(DataContext context) - { - _context = context; - } + public IEnumerable GetPosts() + { + using var connection = new NpgsqlConnection(_connectionString); + var posts = connection.Query("SELECT * FROM public.\"Posts\" ORDER BY \"Id\" ASC").ToList(); - public IEnumerable GetPosts() - { - return _context.Posts.Select(post => post.AsDto()); - } + return posts.Select(post => new PostDto + { + Id = post.Id, + Title = post.Title, + Slug = post.Slug, + Body = post.Body, + CreatedDate = post.CreatedDate + }).ToList(); + } - public PostDto GetPost(string slug) - { - return getPost(slug).AsDto(); - } + public PostDto GetPost(string slug) + { + using var connection = new NpgsqlConnection(_connectionString); + var post = connection.QuerySingleOrDefault("SELECT * FROM public.\"Posts\" WHERE \"Slug\" = @Slug", new { Slug = slug }); + if (post == null) + return null; - private Post getPost(string slug) - { - return _context.Posts.Where(a=>a.Slug==slug.ToString()).SingleOrDefault(); + return new PostDto + { + Id = post.Id, + Title = post.Title, + Slug = post.Slug, + Body = post.Body, + CreatedDate = post.CreatedDate + }; + } + + public PostDto CreatePost(CreatePostDto createPostDto) + { + var newPost = new Post + { + Id = Guid.NewGuid(), + Title = createPostDto.Title, + Slug = string.IsNullOrEmpty(createPostDto.Slug) ? GenerateSlug(createPostDto.Title) : createPostDto.Slug, + Body = createPostDto.Body, + CreatedDate = createPostDto.CreatedDate == default ? DateTime.UtcNow : createPostDto.CreatedDate + }; + + using var connection = new NpgsqlConnection(_connectionString); + connection.Execute("INSERT INTO public.\"Posts\" (\"Id\", \"Title\", \"Slug\", \"Body\", \"CreatedDate\") VALUES (@Id, @Title, @Slug, @Body, @CreatedDate)", newPost); + + return new PostDto + { + Id = newPost.Id, + Title = newPost.Title, + Slug = newPost.Slug, + Body = newPost.Body, + CreatedDate = newPost.CreatedDate + }; + } + + private static string GenerateSlug(string title) + { + return title.ToLower().Replace(" ", "-"); + } } }