diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..95520b4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,24 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +README.md diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e30af3d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,39 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/net5.0/weddingapi.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + }, + { + "name": "Docker .NET Core Launch", + "type": "docker", + "request": "launch", + "preLaunchTask": "docker-run: debug", + "netCore": { + "appProject": "${workspaceFolder}/weddingapi.csproj" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..9237625 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,98 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/weddingapi.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/weddingapi.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/weddingapi.csproj" + ], + "problemMatcher": "$msCompile" + }, + { + "type": "docker-build", + "label": "docker-build: debug", + "dependsOn": [ + "build" + ], + "dockerBuild": { + "tag": "weddingapi:dev", + "target": "base", + "dockerfile": "${workspaceFolder}/Dockerfile", + "context": "${workspaceFolder}", + "pull": true + }, + "netCore": { + "appProject": "${workspaceFolder}/weddingapi.csproj" + } + }, + { + "type": "docker-build", + "label": "docker-build: release", + "dependsOn": [ + "build" + ], + "dockerBuild": { + "tag": "weddingapi:latest", + "dockerfile": "${workspaceFolder}/Dockerfile", + "context": "${workspaceFolder}", + "pull": true + }, + "netCore": { + "appProject": "${workspaceFolder}/weddingapi.csproj" + } + }, + { + "type": "docker-run", + "label": "docker-run: debug", + "dependsOn": [ + "docker-build: debug" + ], + "dockerRun": {}, + "netCore": { + "appProject": "${workspaceFolder}/weddingapi.csproj", + "enableDebugging": true, + "configureSsl": false + } + }, + { + "type": "docker-run", + "label": "docker-run: release", + "dependsOn": [ + "docker-build: release" + ], + "dockerRun": {}, + "netCore": { + "appProject": "${workspaceFolder}/weddingapi.csproj" + } + } + ] +} \ No newline at end of file diff --git a/Config/MongoDbConfig.cs b/Config/MongoDbConfig.cs new file mode 100644 index 0000000..802c7cd --- /dev/null +++ b/Config/MongoDbConfig.cs @@ -0,0 +1,15 @@ +namespace weddingapi.Config +{ + class MongoDbConfig + { + public string Host { get; set; } + public int Port { get; set; } + + public string ConnectionString { + get + { + return $"mongodb://{Host}:{Port}"; + } + } + } +} \ No newline at end of file diff --git a/Controllers/FamilyController.cs b/Controllers/FamilyController.cs new file mode 100644 index 0000000..f67e66c --- /dev/null +++ b/Controllers/FamilyController.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; + +using weddingapi.Models; +using weddingapi.Repositories; +using weddingapi.Dtos; +using weddingapi.Converters; +using System.Threading.Tasks; + +namespace weddingapi.Controllers +{ + [ApiController] + [Route("family")] + public class FamilyController : ControllerBase + { + private readonly IFamilyRepository repository; + + public FamilyController(IFamilyRepository repository) + { + this.repository = repository; + } + + // GET /families + [HttpGet] + public async Task> GetFamiliesAsync() + { + return (await repository.GetFamiliesAsync()).Select(family => family.AsDto()); + } + + // GET /family/{id} + [HttpGet("{id}")] + public async Task> GetFamilyAsync(Guid id) + { + var family = await repository.GetFamilyAsync(id); + + if(family is null) + { + return NotFound(); + } + + return family.AsDto(); + } + + // POST /family + [HttpPost] + public async Task> CreateFamilyAsync(CreateFamilyDto familyDto) + { + Family fam = new Family() + { + Id = Guid.NewGuid(), + CreatedDate = DateTimeOffset.UtcNow, + Name = familyDto.Name, + Members = familyDto.Members.Select(person => new Person() { + IsChild = person.IsChild, + Name = person.Name, + }).ToList(), + }; + + await repository.CreateFamilyAsync(fam); + + return CreatedAtAction(nameof(GetFamilyAsync), new { id = fam.Id }, fam.AsDto()); + } + + // PUT /family/{id} + [HttpPut("{id}")] + public async Task UpdateFamilyAsync(Guid id, UpdateFamilyDto familyDto) + { + var exisitingFamily = await repository.GetFamilyAsync(id); + + if(exisitingFamily is null) + { + return NotFound(); + } + + Family updatedFamily = exisitingFamily with + { + Members = familyDto.Members.Select(person => new Person() { + Name = person.Name, + IsChild = person.IsChild, + }).ToList(), + Name = familyDto.Name, + }; + + await repository.UpdateFamilyAsync(updatedFamily); + + return NoContent(); + } + + // Delete /family/{id} + [HttpDelete("{id}")] + public async Task DeleteFamilyAsync(Guid id) + { + var exisitingFamily = repository.GetFamilyAsync(id); + + if(exisitingFamily is null) + { + return NotFound(); + } + + await repository.DeleteFamilyAsync(id); + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/Converters/Extensions.cs b/Converters/Extensions.cs new file mode 100644 index 0000000..68f5994 --- /dev/null +++ b/Converters/Extensions.cs @@ -0,0 +1,30 @@ +using System.Linq; +using weddingapi.Dtos; +using weddingapi.Models; + +namespace weddingapi.Converters +{ + public static class Converters + { + public static FamilyDto AsDto(this Family family) + { + return new FamilyDto() + { + Id = family.Id, + CreatedDate = family.CreatedDate, + Members = family.Members.Select(person => person.AsDto()).ToList(), + Name = family.Name, + }; + } + + public static PersonDto AsDto(this Person person) + { + return new PersonDto() + { + IsChild = person.IsChild, + Name = person.Name, + }; + } + } +} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ab0c012 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal AS base +WORKDIR /app +EXPOSE 80 + +ENV ASPNETCORE_URLS=http://+:80 + +FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +WORKDIR /src +COPY ["weddingapi.csproj", "./"] +RUN dotnet restore "weddingapi.csproj" +COPY . . +RUN dotnet publish "weddingapi.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "weddingapi.dll"] diff --git a/Dtos/CreateFamilyDto.cs b/Dtos/CreateFamilyDto.cs new file mode 100644 index 0000000..8ce6f75 --- /dev/null +++ b/Dtos/CreateFamilyDto.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace weddingapi.Dtos +{ + public record CreateFamilyDto + { + [Required] + public string Name { get; init; } + [Required] + public IList Members { get; init;} + } +} \ No newline at end of file diff --git a/Dtos/FamilyDto.cs b/Dtos/FamilyDto.cs new file mode 100644 index 0000000..a28d37f --- /dev/null +++ b/Dtos/FamilyDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace weddingapi.Dtos +{ + public record FamilyDto + { + public Guid Id { get; init; } + + public string Name { get; init; } + + public DateTimeOffset CreatedDate { get; set; } + + public IList Members { get; init;} + } +} \ No newline at end of file diff --git a/Dtos/PersonDto.cs b/Dtos/PersonDto.cs new file mode 100644 index 0000000..78fcb8c --- /dev/null +++ b/Dtos/PersonDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace weddingapi.Dtos +{ + public record PersonDto + { + public string Name { get; init; } + + public bool IsChild { get; set; } + } +} \ No newline at end of file diff --git a/Dtos/UpdateFamilyDto.cs b/Dtos/UpdateFamilyDto.cs new file mode 100644 index 0000000..04bd29d --- /dev/null +++ b/Dtos/UpdateFamilyDto.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace weddingapi.Dtos +{ + public record UpdateFamilyDto + { + [Required] + public string Name { get; init; } + [Required] + public IList Members { get; init;} + } +} \ No newline at end of file diff --git a/Models/Family.cs b/Models/Family.cs new file mode 100644 index 0000000..ceab1d1 --- /dev/null +++ b/Models/Family.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace weddingapi.Models +{ + public record Family + { + public Guid Id { get; init; } + + public string Name { get; init; } + + public DateTimeOffset CreatedDate { get; set; } + + public IList Members { get; init; } + + public Family() + { + this.Members = new List(); + } + } +} \ No newline at end of file diff --git a/Models/Person.cs b/Models/Person.cs new file mode 100644 index 0000000..d752e00 --- /dev/null +++ b/Models/Person.cs @@ -0,0 +1,11 @@ +using System; + +namespace weddingapi.Models +{ + public record Person + { + public string Name { get; init; } + + public bool IsChild { get; set; } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..8386c19 --- /dev/null +++ b/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace weddingapi +{ + 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/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..98581d0 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50382", + "sslPort": 44326 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "weddingapi": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbf343d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +https://www.youtube.com/watch?v=bgk8N_rx1F4 \ No newline at end of file diff --git a/Repositories/IFamilyRepository.cs b/Repositories/IFamilyRepository.cs new file mode 100644 index 0000000..8060d2c --- /dev/null +++ b/Repositories/IFamilyRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using weddingapi.Models; + +namespace weddingapi.Repositories +{ + public interface IFamilyRepository + { + Task GetFamilyAsync(Guid id); + + Task> GetFamiliesAsync(); + + Task CreateFamilyAsync(Family family); + + Task UpdateFamilyAsync(Family family); + + Task DeleteFamilyAsync(Guid id); + } +} \ No newline at end of file diff --git a/Repositories/InMemFamilyRepository.cs b/Repositories/InMemFamilyRepository.cs new file mode 100644 index 0000000..ad6c053 --- /dev/null +++ b/Repositories/InMemFamilyRepository.cs @@ -0,0 +1,44 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using weddingapi.Models; + +// namespace weddingapi.Repositories +// { +// public class InMemFamilyRepository : IFamilyRepository +// { +// private readonly List families = new() +// { +// new Family { Id = Guid.NewGuid(), Name = "Lol", CreatedDate = DateTimeOffset.UtcNow }, +// new Family { Id = Guid.NewGuid(), Name = "Lol1", CreatedDate = DateTimeOffset.UtcNow }, +// new Family { Id = Guid.NewGuid(), Name = "Lol2", CreatedDate = DateTimeOffset.UtcNow } +// }; + +// public IEnumerable GetFamiliesAsync() +// { +// return families; +// } + +// public Family GetFamilyAsync(Guid id) +// { +// return families.Where(family => family.Id == id).SingleOrDefault(); +// } + +// public void CreateFamilyAsync(Family family) +// { +// this.families.Add(family); +// } + +// public void UpdateFamilyAsync(Family family) +// { +// var index = families.FindIndex(existingFamily => existingFamily.Id == family.Id); +// families[index] = family; +// } + +// public void DeleteFamilyAsync(Guid id) +// { +// var index = families.FindIndex(existingFamily => existingFamily.Id == id); +// families.RemoveAt(index); +// } +// } +// } \ No newline at end of file diff --git a/Repositories/MongoDBFamilyRepository.cs b/Repositories/MongoDBFamilyRepository.cs new file mode 100644 index 0000000..94d262a --- /dev/null +++ b/Repositories/MongoDBFamilyRepository.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using weddingapi.Models; + +namespace weddingapi.Repositories +{ + public class MongoDBFamilyRepository : IFamilyRepository + { + private const string databaseName = "wedding"; + private const string collectionName = "families"; + private readonly IMongoCollection familyCollection; + private readonly FilterDefinitionBuilder filterBuilder = Builders.Filter; + + public MongoDBFamilyRepository(IMongoClient mongoClient) + { + IMongoDatabase database = mongoClient.GetDatabase(databaseName); + familyCollection = database.GetCollection(collectionName); + } + + public async Task CreateFamilyAsync(Family family) + { + await familyCollection.InsertOneAsync(family); + } + + public async Task DeleteFamilyAsync(Guid id) + { + var filter = filterBuilder.Eq(family => family.Id, id); + await familyCollection.DeleteOneAsync(filter); + } + + public async Task> GetFamiliesAsync() + { + var res = await familyCollection.FindAsync(new BsonDocument()); + return await res.ToListAsync(); + } + + public async Task GetFamilyAsync(Guid id) + { + var filter = filterBuilder.Eq(family => family.Id, id); + var res = await familyCollection.FindAsync(filter); + return await res.SingleOrDefaultAsync(); + } + + public async Task UpdateFamilyAsync(Family family) + { + var filter = filterBuilder.Eq(exisitingFamily => exisitingFamily.Id, family.Id); + await familyCollection.ReplaceOneAsync(filter, family); + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..b4d3d9e --- /dev/null +++ b/Startup.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using weddingapi.Config; +using weddingapi.Repositories; + +namespace weddingapi +{ + 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) + { + BsonSerializer.RegisterSerializer(new GuidSerializer(BsonType.String)); + BsonSerializer.RegisterSerializer(new DateTimeOffsetSerializer(BsonType.String)); + + services.AddSingleton(serviceProvider => + { + var config = Configuration.GetSection(nameof(MongoDbConfig)).Get(); + return new MongoClient(config.ConnectionString); + }); + + services.AddSingleton(); + services.AddControllers(options => { + options.SuppressAsyncSuffixInActionNames = false; + }); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "weddingapi", Version = "v1" }); + }); + } + + // 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.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "weddingapi v1")); + } + + if(env.IsDevelopment()) + { + app.UseHttpsRedirection(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/WeatherForecast.cs b/WeatherForecast.cs new file mode 100644 index 0000000..d139d82 --- /dev/null +++ b/WeatherForecast.cs @@ -0,0 +1,15 @@ +using System; + +namespace weddingapi +{ + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string Summary { get; set; } + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..27ec8cb --- /dev/null +++ b/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "MongoDbConfig": { + "Host": "localhost", + "Port": "27017" + } +} diff --git a/kubernetes/weddingapi.yaml b/kubernetes/weddingapi.yaml new file mode 100644 index 0000000..4e845f7 --- /dev/null +++ b/kubernetes/weddingapi.yaml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: weddingapi-deployment +spec: + selector: + matchLabels: + app: weddingapi + template: + metadata: + labels: + app: weddingapi + spec: + containers: + - name: weddingapi + image: weddingapi:v1 + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 80 + env: + - name: MongoDbConfig__Host + value: mongodb-service + + +--- +apiVersion: v1 +kind: Service +metadata: + name: weddingapi-service +spec: + type: LoadBalancer + selector: + app: weddingapi + ports: + - port: 11180 #external port + targetPort: 80 #container port diff --git a/weddingapi.csproj b/weddingapi.csproj new file mode 100644 index 0000000..0f549b6 --- /dev/null +++ b/weddingapi.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + +