From b23e96493cfb601f99c2b25cdfbbc99bc5c94bbf Mon Sep 17 00:00:00 2001 From: Robert Marshall Date: Fri, 10 Apr 2020 09:28:43 +0100 Subject: [PATCH] Copy blog code to new API micro service repo --- .editorconfig | 198 ++++++++++++++++++ .gitattributes | 1 + .gitconfig | 2 + .gitignore | 55 +++++ Readme.md | 7 + clean-config.sh | 5 + src/Robware.Api.Blog.sln | 48 +++++ .../Controllers/BlogController.cs | 53 +++++ src/Robware.Api.Blog/Program.cs | 16 ++ .../Properties/launchSettings.json | 30 +++ src/Robware.Api.Blog/Robware.Api.Blog.csproj | 13 ++ src/Robware.Api.Blog/Startup.cs | 43 ++++ .../appsettings.Development.json | 12 ++ src/Robware.Api.Blog/appsettings.json | 10 + src/Robware.Blog.Tests/BlogPostTests.cs | 42 ++++ .../Robware.Blog.Tests.csproj | 21 ++ src/Robware.Blog/BlogPost.cs | 28 +++ src/Robware.Blog/IBlogRepository.cs | 15 ++ src/Robware.Blog/Robware.Blog.csproj | 7 + src/Robware.Data/BlogRepository.cs | 97 +++++++++ src/Robware.Data/IDatabaseProvider.cs | 9 + src/Robware.Data/MySQLDatabaseProvider.cs | 14 ++ src/Robware.Data/Robware.Data.csproj | 18 ++ src/Robware.Data/States/BlogPostState.cs | 19 ++ 24 files changed, 763 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitconfig create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 clean-config.sh create mode 100644 src/Robware.Api.Blog.sln create mode 100644 src/Robware.Api.Blog/Controllers/BlogController.cs create mode 100644 src/Robware.Api.Blog/Program.cs create mode 100644 src/Robware.Api.Blog/Properties/launchSettings.json create mode 100644 src/Robware.Api.Blog/Robware.Api.Blog.csproj create mode 100644 src/Robware.Api.Blog/Startup.cs create mode 100644 src/Robware.Api.Blog/appsettings.Development.json create mode 100644 src/Robware.Api.Blog/appsettings.json create mode 100644 src/Robware.Blog.Tests/BlogPostTests.cs create mode 100644 src/Robware.Blog.Tests/Robware.Blog.Tests.csproj create mode 100644 src/Robware.Blog/BlogPost.cs create mode 100644 src/Robware.Blog/IBlogRepository.cs create mode 100644 src/Robware.Blog/Robware.Blog.csproj create mode 100644 src/Robware.Data/BlogRepository.cs create mode 100644 src/Robware.Data/IDatabaseProvider.cs create mode 100644 src/Robware.Data/MySQLDatabaseProvider.cs create mode 100644 src/Robware.Data/Robware.Data.csproj create mode 100644 src/Robware.Data/States/BlogPostState.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f88f151 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,198 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +[*] +indent_style = tab + +# C# files +[*.cs] +max_line_length = 180 +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:suggestion + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false:silent +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = false:silent + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent + +# Code-block preferences +csharp_prefer_braces = false:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{yaml,yml}] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9c49a7f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/Website/appsettings.Development.json filter=clean-config \ No newline at end of file diff --git a/.gitconfig b/.gitconfig new file mode 100644 index 0000000..169b515 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,2 @@ +[filter "clean-config"] + clean = ./clean-config.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2ad9c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc +nupkg/ + +# Visual Studio Code +.vscode + +# Rider +.idea + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +#build/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ + + +#lcov files +lcov.info + +#compiled and minified assets +*.css +*.min.css +*.min.js + +#nodejs +node_modules + +output +.tmp +*.ncrunchsolution +/_NCrunch_Website +*.DotSettings \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..52785d3 --- /dev/null +++ b/Readme.md @@ -0,0 +1,7 @@ +# Blog microservice + +Provides an API to save and load blog posts + +## Setup + +After clone, please run `git config --local include.path ../.gitconfig`. This will set up the filters required to ignore local dev config. \ No newline at end of file diff --git a/clean-config.sh b/clean-config.sh new file mode 100644 index 0000000..581bc42 --- /dev/null +++ b/clean-config.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sed \ +-e 's/"database": "Server=.*;User ID=.*;Password=.*;Database=.*"/"database": "Server=localhost;User ID=user;Password=pass;Database=db"/g' \ +$1 \ No newline at end of file diff --git a/src/Robware.Api.Blog.sln b/src/Robware.Api.Blog.sln new file mode 100644 index 0000000..5e5a5e3 --- /dev/null +++ b/src/Robware.Api.Blog.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robware.Api.Blog", "Robware.Api.Blog\Robware.Api.Blog.csproj", "{C5B2C08C-423C-494D-8C2B-77F2C73F36CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Blog", "Robware.Blog\Robware.Blog.csproj", "{92F5058F-8F58-4A4D-A661-49A4E1198677}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Data", "Robware.Data\Robware.Data.csproj", "{8860B6F7-8740-4CF5-BF24-67D00CA7C1A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Blog.Tests", "Robware.Blog.Tests\Robware.Blog.Tests.csproj", "{EEB41C17-B61C-451A-9C72-2B405DC42743}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8448EA1E-66F2-4566-8EBC-FDE8FE9BD733}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C5B2C08C-423C-494D-8C2B-77F2C73F36CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5B2C08C-423C-494D-8C2B-77F2C73F36CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5B2C08C-423C-494D-8C2B-77F2C73F36CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5B2C08C-423C-494D-8C2B-77F2C73F36CC}.Release|Any CPU.Build.0 = Release|Any CPU + {92F5058F-8F58-4A4D-A661-49A4E1198677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92F5058F-8F58-4A4D-A661-49A4E1198677}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92F5058F-8F58-4A4D-A661-49A4E1198677}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92F5058F-8F58-4A4D-A661-49A4E1198677}.Release|Any CPU.Build.0 = Release|Any CPU + {8860B6F7-8740-4CF5-BF24-67D00CA7C1A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8860B6F7-8740-4CF5-BF24-67D00CA7C1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8860B6F7-8740-4CF5-BF24-67D00CA7C1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8860B6F7-8740-4CF5-BF24-67D00CA7C1A3}.Release|Any CPU.Build.0 = Release|Any CPU + {EEB41C17-B61C-451A-9C72-2B405DC42743}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEB41C17-B61C-451A-9C72-2B405DC42743}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEB41C17-B61C-451A-9C72-2B405DC42743}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEB41C17-B61C-451A-9C72-2B405DC42743}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6B846E24-8B8F-4472-AAF1-5500B4C6303F} + EndGlobalSection +EndGlobal diff --git a/src/Robware.Api.Blog/Controllers/BlogController.cs b/src/Robware.Api.Blog/Controllers/BlogController.cs new file mode 100644 index 0000000..f83ea5b --- /dev/null +++ b/src/Robware.Api.Blog/Controllers/BlogController.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Robware.Blog; + +namespace Robware.Api.Blog.Controllers { + [ApiController] + public class BlogController : ControllerBase { + private readonly ILogger _logger; + private readonly IBlogRepository _blogRepository; + + public BlogController(ILogger logger, IBlogRepository blogRepository) { + _logger = logger; + _blogRepository = blogRepository; + } + + [HttpGet(nameof(Get) + "/{url}")] + public async Task Get(string url) { + _logger.Log(LogLevel.Information, $"{nameof(Get)}: {nameof(url)}={url}"); + + if (int.TryParse(url, out int id)) { + return await _blogRepository.GetPostByIdAsync(id); + } + + return await _blogRepository.GetPostByUrlAsync(url); + } + + //[HttpGet] + //Task GetPostByUrl(); + + //[HttpGet] + //Task> GetLatestPosts(int limit, int offset = 0); + + //[HttpGet] + //Task GetLatestPost(); + + //[HttpGet] + //Task GetCount(); + + //[HttpGet] + //Task GetPostById(int id); + + //[HttpPost] + //Task SavePost(BlogPost post); + + //[HttpGet] + //Task> GetAllPosts(); + + //[HttpGet] + //Task DeletePost(int id); + } +} diff --git a/src/Robware.Api.Blog/Program.cs b/src/Robware.Api.Blog/Program.cs new file mode 100644 index 0000000..246b14c --- /dev/null +++ b/src/Robware.Api.Blog/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Robware.Api.Blog { + public class Program { + public static void Main(string[] args) { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Robware.Api.Blog/Properties/launchSettings.json b/src/Robware.Api.Blog/Properties/launchSettings.json new file mode 100644 index 0000000..2b22325 --- /dev/null +++ b/src/Robware.Api.Blog/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53729", + "sslPort": 44379 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "blog", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Robware.Api.Blog": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/Robware.Api.Blog/Robware.Api.Blog.csproj b/src/Robware.Api.Blog/Robware.Api.Blog.csproj new file mode 100644 index 0000000..8ae137f --- /dev/null +++ b/src/Robware.Api.Blog/Robware.Api.Blog.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/src/Robware.Api.Blog/Startup.cs b/src/Robware.Api.Blog/Startup.cs new file mode 100644 index 0000000..22fe03b --- /dev/null +++ b/src/Robware.Api.Blog/Startup.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Robware.Blog; +using Robware.Data; + +namespace Robware.Api.Blog { + public class Startup { + public Startup(IConfiguration configuration) { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) { + services.AddControllers(); + + services + .AddSingleton(new MySQLDatabaseProvider(Configuration.GetConnectionString("database"))) + .AddSingleton(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + if (env.IsDevelopment()) { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Robware.Api.Blog/appsettings.Development.json b/src/Robware.Api.Blog/appsettings.Development.json new file mode 100644 index 0000000..6daac7a --- /dev/null +++ b/src/Robware.Api.Blog/appsettings.Development.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ConnectionStrings": { + "database": "Server=localhost;User ID=user;Password=pass;Database=db" + } +} diff --git a/src/Robware.Api.Blog/appsettings.json b/src/Robware.Api.Blog/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/src/Robware.Api.Blog/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Robware.Blog.Tests/BlogPostTests.cs b/src/Robware.Blog.Tests/BlogPostTests.cs new file mode 100644 index 0000000..6797925 --- /dev/null +++ b/src/Robware.Blog.Tests/BlogPostTests.cs @@ -0,0 +1,42 @@ +using FluentAssertions; +using Robware.Blog; +using Xunit; + +namespace Website.Tests.Models { + public class BlogPostTests { + [Fact] + public void UpdateTitle_WithNewTitle_UpdatesTitlePropertyAndRegeneratesUrl() { + var post = new BlogPost(); + post.UpdateTitle("new title"); + + post.Title.Should().Be("new title"); + post.Url.Should().Be("new-title"); + } + + [Theory] + [InlineData("new:title", "new-title")] + [InlineData("new: title", "new-title")] + [InlineData("new$title", "new-title")] + [InlineData("new TITle", "new-title")] + public void UpdateTitle_WithUnsafeCharsInTitle_RegeneratesSafeUrl(string title, string url) { + var post = new BlogPost(); + post.UpdateTitle(title); + post.Url.Should().Be(url); + } + + [Fact] + public void UpdateDraft_WithContent_UpdatesDraftProperty() { + var post = new BlogPost(); + post.UpdateDraft("content"); + post.Draft.Should().Be("content"); + } + + [Fact] + public void Publish_SetsContentToDraft() { + var post = new BlogPost(); + post.UpdateDraft("content"); + post.Publish(); + post.Content.Should().Be("content"); + } + } +} diff --git a/src/Robware.Blog.Tests/Robware.Blog.Tests.csproj b/src/Robware.Blog.Tests/Robware.Blog.Tests.csproj new file mode 100644 index 0000000..1e8c695 --- /dev/null +++ b/src/Robware.Blog.Tests/Robware.Blog.Tests.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + diff --git a/src/Robware.Blog/BlogPost.cs b/src/Robware.Blog/BlogPost.cs new file mode 100644 index 0000000..8447c87 --- /dev/null +++ b/src/Robware.Blog/BlogPost.cs @@ -0,0 +1,28 @@ +using System; +using System.Text.RegularExpressions; + +namespace Robware.Blog +{ + public class BlogPost + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime Timestamp { get; set; } + public string Draft { get; set; } + public string Url { get; set; } + public int UserId { get; set; } + + private void GenerateUrl() { + Url = Regex.Replace(Title, @"[^a-zA-Z0-9\.]+", "-").ToLower(); + } + + public void UpdateTitle(string title) { + Title = title; + GenerateUrl(); + } + + public void UpdateDraft(string content) => Draft = content; + public void Publish() => Content = Draft; + } +} diff --git a/src/Robware.Blog/IBlogRepository.cs b/src/Robware.Blog/IBlogRepository.cs new file mode 100644 index 0000000..17c730e --- /dev/null +++ b/src/Robware.Blog/IBlogRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Robware.Blog { + public interface IBlogRepository { + Task GetPostByUrlAsync(string url); + Task> GetLatestPostsAsync(int limit, int offset = 0); + Task GetLatestPostAsync(); + Task GetCountAsync(); + Task GetPostByIdAsync(int id); + Task SavePost(BlogPost post); + Task> GetAllPostsAsync(); + Task DeletePostAsync(int id); + } +} diff --git a/src/Robware.Blog/Robware.Blog.csproj b/src/Robware.Blog/Robware.Blog.csproj new file mode 100644 index 0000000..cb63190 --- /dev/null +++ b/src/Robware.Blog/Robware.Blog.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/src/Robware.Data/BlogRepository.cs b/src/Robware.Data/BlogRepository.cs new file mode 100644 index 0000000..116ee30 --- /dev/null +++ b/src/Robware.Data/BlogRepository.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using Robware.Blog; +using Robware.Data.States; + +namespace Robware.Data +{ + public class BlogRepository : IBlogRepository + { + private readonly IDatabaseProvider _dbProvider; + + public BlogRepository(IDatabaseProvider dbProvider) => _dbProvider = dbProvider; + + private static BlogPost MapBlogPost(BlogPostState state) => new BlogPost { + Id = state.Post_Id, + Title = state.Post_Title, + Content = state.Post_Content, + Timestamp = state.Post_Timestamp, + Draft = state.Post_Draft, + Url = state.Post_Url, + UserId = state.User_Id + }; + + public async Task GetPostByUrlAsync(string url) { + const string query = "SELECT * FROM blog_posts WHERE post_url=@url AND post_deleted=0"; + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(query, new{url}); + return MapBlogPost(result.First()); + } + } + + public async Task> GetLatestPostsAsync(int limit, int offset = 0) { + const string query = "SELECT * FROM blog_posts WHERE post_content<>'' AND post_deleted=0 ORDER BY post_timestamp DESC LIMIT @offset,@limit"; + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var results = await connection.QueryAsync(query, new{limit, offset}); + return results.Select(MapBlogPost); + } + } + + public async Task GetLatestPostAsync() => (await GetLatestPostsAsync(1)).First(); + + public async Task GetCountAsync() + { + var query="SELECT COUNT(*) FROM blog_posts WHERE post_content<>'' AND post_deleted=0"; + using(var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(query); + return result.First(); + } + } + + public async Task GetPostByIdAsync(int id) { + const string query = "SELECT * FROM blog_posts WHERE post_id=@id AND post_deleted=0"; + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(query, new {id}); + return MapBlogPost(result.First()); + } + } + + public async Task SavePost(BlogPost post) { + const string newPostQuery = "INSERT INTO blog_posts (post_title, post_content, post_draft, post_url) VALUES (@title, @content, @draft, @url); SELECT CAST(LAST_INSERT_ID() as int)"; + const string updatePostQuery = "UPDATE blog_posts SET post_id = @id, post_title = @title, post_content = @content, post_draft = @draft, post_url = @url WHERE post_id = @id "; + + var newPost = post.Id == 0; + + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(newPost ? newPostQuery : updatePostQuery, post); + return newPost ? await GetPostByIdAsync(result.Single()) : post; + } + } + + public async Task> GetAllPostsAsync() { + const string query = "SELECT * FROM blog_posts WHERE post_deleted=0"; + + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + var result = await connection.QueryAsync(query); + return result.Select(MapBlogPost); + } + } + + public async Task DeletePostAsync(int id) { + const string query = "UPDATE blog_posts SET post_deleted=1 WHERE post_id=@id"; + + using (var connection = _dbProvider.NewConnection()) { + connection.Open(); + await connection.ExecuteAsync(query, new {id}); + } + } + } +} diff --git a/src/Robware.Data/IDatabaseProvider.cs b/src/Robware.Data/IDatabaseProvider.cs new file mode 100644 index 0000000..7cff514 --- /dev/null +++ b/src/Robware.Data/IDatabaseProvider.cs @@ -0,0 +1,9 @@ +using System.Data; + +namespace Robware.Data +{ + public interface IDatabaseProvider + { + IDbConnection NewConnection(); + } +} diff --git a/src/Robware.Data/MySQLDatabaseProvider.cs b/src/Robware.Data/MySQLDatabaseProvider.cs new file mode 100644 index 0000000..1fab056 --- /dev/null +++ b/src/Robware.Data/MySQLDatabaseProvider.cs @@ -0,0 +1,14 @@ +using System.Data; +using MySql.Data.MySqlClient; + +namespace Robware.Data +{ + public class MySQLDatabaseProvider:IDatabaseProvider + { + private readonly string _connectionString; + + public MySQLDatabaseProvider(string connectionString) => _connectionString = connectionString; + + public IDbConnection NewConnection() => new MySqlConnection(_connectionString); + } +} diff --git a/src/Robware.Data/Robware.Data.csproj b/src/Robware.Data/Robware.Data.csproj new file mode 100644 index 0000000..c765d7f --- /dev/null +++ b/src/Robware.Data/Robware.Data.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + diff --git a/src/Robware.Data/States/BlogPostState.cs b/src/Robware.Data/States/BlogPostState.cs new file mode 100644 index 0000000..d53f348 --- /dev/null +++ b/src/Robware.Data/States/BlogPostState.cs @@ -0,0 +1,19 @@ +using System; +using Dapper.Contrib.Extensions; + +namespace Robware.Data.States +{ + [Table("blog_posts")] + public class BlogPostState + { + [Key] + public int Post_Id { get; set; } + public string Post_Title { get; set; } + public string Post_Content { get; set; } + public DateTime Post_Timestamp { get; set; } + public string Post_Draft { get; set; } + public string Post_Url { get; set; } + public bool Post_Deleted{ get; set; } + public int User_Id { get; set; } + } +}