initial commit

This commit is contained in:
2026-03-20 23:52:10 +01:00
parent 05bea695bd
commit ce04cd8d77
38 changed files with 3006 additions and 52 deletions

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
@@ -6,9 +6,33 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<Optimize>False</Optimize>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" /> <PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.4" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="serilog.sinks.console" Version="6.1.1" />
<PackageReference Include="serilog.sinks.file" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.5" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>
<ItemGroup>
<None Update="email_template.pdf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,6 +0,0 @@
@CityInfo.API_HostAddress = http://localhost:5244
GET {{CityInfo.API_HostAddress}}/weatherforecast/
Accept: application/json
###

BIN
CityInfo.API/CityInfo.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace CityInfo.API.Controllers
{
[Route("api/authentication")]
[ApiController]
public class AuthenticationController : ControllerBase
{
private readonly IConfiguration _configuration;
public class AuthenticationRequestBody
{
public string? UserName { get; set; }
public string? Password { get; set; }
}
internal class CityInfoUser
{
public int UserId { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public CityInfoUser(
int userId, string userName, string firstName, string lastName, string city)
{
UserId = userId;
UserName = userName;
FirstName = firstName;
LastName = lastName;
City = city;
}
}
public AuthenticationController(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
[HttpPost("authenticate")]
public ActionResult<string> Authenticate(AuthenticationRequestBody authenticationRequestBody)
{
var user = ValidateUserCredentials(authenticationRequestBody.UserName, authenticationRequestBody.Password);
if (user == null)
{
return Unauthorized();
}
var securityKey = new SymmetricSecurityKey(Convert.FromBase64String(_configuration["Authentication:SecretForKey"]));
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claimsForToken = new List<Claim>
{
new Claim("sub", user.UserId.ToString()),
new Claim("given_name", user.FirstName),
new Claim("family_name", user.LastName),
new Claim("city", user.City)
};
var jwtSecurityToken = new JwtSecurityToken(
_configuration["Authentication:Issuer"],
_configuration["Authentication:Audience"],
claimsForToken,
DateTime.UtcNow,
DateTime.UtcNow.AddHours(1),
signingCredentials);
var tokenToReturn = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return Ok(tokenToReturn);
}
private CityInfoUser ValidateUserCredentials(string? userName, string? password)
{
return new CityInfoUser(
1,
userName ?? "",
"Nathan",
"Pire",
"Charleroi");
}
}
}

View File

@@ -0,0 +1,63 @@
using AutoMapper;
using CityInfo.API.Models;
using CityInfo.API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;
namespace CityInfo.API.Controllers
{
[ApiController]
[Authorize]
[Route("api/cities")]
public class CitiesController: ControllerBase
{
private readonly ICityInfoRepository _cityInfoRepository;
private readonly IMapper _mapper;
const int maximumPageSize = 20;
public CitiesController(
ICityInfoRepository cityInfoRepository,
IMapper mapper)
{
_cityInfoRepository = cityInfoRepository ?? throw new ArgumentNullException(nameof(cityInfoRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
[HttpGet()]
public async Task<ActionResult<IEnumerable<CityWithoutPointsOfInterestDto>>> GetCities(
string? name, string? searchQuery, int pageNumber = 1, int pageSize = 10)
{
if(pageSize > maximumPageSize)
{
pageSize = maximumPageSize;
}
var (cityEntities, paginationMetaData) = await _cityInfoRepository
.GetCitiesAsync(name, searchQuery,pageNumber, pageSize);
Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(paginationMetaData));
return Ok(_mapper.Map<IEnumerable<CityWithoutPointsOfInterestDto>>(cityEntities));
}
[HttpGet("{id}")]
public async Task<IActionResult> GetCity(int id, bool includePointsOfInterest = false)
{
var city = await _cityInfoRepository.GetCityAsync(id, includePointsOfInterest);
if (city == null)
{
return NotFound();
}
if (includePointsOfInterest)
{
return Ok(_mapper.Map<CityDto>(city));
}
return Ok(_mapper.Map<CityWithoutPointsOfInterestDto>(city));
}
}
}

View File

@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
namespace CityInfo.API.Controllers
{
[Route("api/files")]
[Authorize]
[ApiController]
public class FilesController : ControllerBase
{
private readonly FileExtensionContentTypeProvider _fileExtensionContentTypeProvide;
public FilesController(
FileExtensionContentTypeProvider fileExtensionContentTypeProvider)
{
_fileExtensionContentTypeProvide = fileExtensionContentTypeProvider
?? throw new System.ArgumentNullException(
nameof(FileExtensionContentTypeProvider));
}
[HttpGet("{fileId}")]
public ActionResult GetFile(string fileId)
{
var pathToFile = "email_template.pdf";
if (!System.IO.File.Exists(pathToFile))
{
return NotFound();
}
if (!_fileExtensionContentTypeProvide
.TryGetContentType(pathToFile, out var contentType))
{
contentType = "application/octet-stream";
}
var bytes = System.IO.File.ReadAllBytes(pathToFile);
return File(bytes, contentType , Path.GetFileName(pathToFile));
}
[HttpPost]
public async Task<ActionResult> CreateFile(IFormFile file)
{
if(file.Length == 0 || file.Length > 20971520 || file.ContentType != "application/pdf")
{
return BadRequest("No file or an invalid one has been inputted.");
}
var path = Path.Combine(
Directory.GetCurrentDirectory(),
$"uploaded_file_{Guid.NewGuid()}.pdf");
using (var stream = new FileStream(path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok("Your file has been uploaded successfully.");
}
}
}

View File

@@ -0,0 +1,183 @@
using AutoMapper;
using CityInfo.API.Entities;
using CityInfo.API.Models;
using CityInfo.API.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
namespace CityInfo.API.Controllers
{
[Route("api/cities/{cityId}/pointsofinterest/", Name = "GetPointsOfInterest")]
[Authorize(Policy = "MustBeFromCharleroi")]
[ApiController]
public class PointsOfInterestController : ControllerBase
{
private readonly ILogger<PointsOfInterestController> _logger;
private readonly IMailService _mailService;
private readonly ICityInfoRepository _cityInfoRepository;
private readonly IMapper _mapper;
public PointsOfInterestController(
ILogger<PointsOfInterestController> logger,
IMailService mailService,
ICityInfoRepository cityInfoRepository,
IMapper mapper)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mailService = mailService ?? throw new ArgumentNullException(nameof(mailService));
_cityInfoRepository = cityInfoRepository ?? throw new ArgumentNullException(nameof(cityInfoRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
[HttpGet]
public async Task<ActionResult<IEnumerable<PointOfInterestDto>>> GetPointsOfInterest(int cityId)
{
//throw new Exception("pipi");
try
{
//throw new Exception("caca prout");
var cityName = User.Claims.FirstOrDefault(claim => claim.Type == "city")?.Value;
if (!await _cityInfoRepository.CityNameMatchesCityId(cityName, cityId))
{
return Forbid();
}
if(!await _cityInfoRepository.CityExistAsync(cityId))
{
_logger.LogInformation($"City with Id {cityId} wasn't found when accessing points of interest.");
return NotFound();
}
var pointsOfInterest = await _cityInfoRepository.GetPointsOfinterestForCityAsync(cityId);
return Ok(_mapper.Map<IEnumerable<PointOfInterestDto>>(pointsOfInterest));
}
catch (Exception ex)
{
_logger.LogCritical($"Exception while getting points of interest for city with id {cityId}", ex);
return StatusCode(500, "A problem happened while handling your request.");
}
}
[HttpGet("{pointOfInterestId}", Name = "GetPointOfInterest")]
public async Task<ActionResult<PointOfInterestDto>> GetPointOfInterest(int cityId, int pointOfInterestId)
{
if (!await _cityInfoRepository.CityExistAsync(cityId))
{
_logger.LogInformation($"City with Id {cityId} wasn't found when accessing point of interest.");
return NotFound();
}
var pointOfInterest = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId);
if (pointOfInterest == null)
{
return NotFound();
}
return Ok(_mapper.Map<PointOfInterestDto>(pointOfInterest));
}
[HttpPost(Name = "CreatePointOfInterest")]
public async Task<ActionResult<PointOfInterestDto>> CreatePointOfInterest(
int cityId,
PointOfInterestForCreationDto pointOfInterest)
{
if (!await _cityInfoRepository.CityExistAsync(cityId))
{
return NotFound();
}
var finalPointOfInterest = _mapper.Map<PointOfInterest>(pointOfInterest);
await _cityInfoRepository.CreatePointOfInterestForCityAsync(cityId, finalPointOfInterest);
await _cityInfoRepository.SaveChangesAsync();
var createdPointOfInterestToReturn = _mapper.Map<PointOfInterestDto>(finalPointOfInterest);
return CreatedAtRoute("GetPointOfInterest",
new
{
cityId = cityId,
pointOfInterestId = createdPointOfInterestToReturn.Id
},
createdPointOfInterestToReturn);
}
[HttpPut("{pointOfInterestId}", Name = "UpdatePointOfInterest")]
public async Task<ActionResult> UpdatePointOfInterest(
int cityId,
int pointOfInterestId,
PointOfInterestForUpdateDto pointOfInterest)
{
if (!await _cityInfoRepository.CityExistAsync(cityId))
{
return NotFound();
}
var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId);
if (pointofInterestEntity == null)
{
return NotFound();
}
_mapper.Map(pointOfInterest, pointofInterestEntity);
await _cityInfoRepository.SaveChangesAsync();
return NoContent();
}
[HttpPatch("{pointOfInterestId}", Name = "PartiallyUpdatePointOfInterest")]
public async Task<ActionResult> PartiallyUpdatePointOfInterest(
int cityId,
int pointOfInterestId,
JsonPatchDocument<PointOfInterestForUpdateDto> patchDocument)
{
if (!await _cityInfoRepository.CityExistAsync(cityId))
{
return NotFound();
}
var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId);
if (pointofInterestEntity == null)
{
return NotFound();
}
var pointOfInterestToPatch = _mapper.Map<PointOfInterestForUpdateDto>(pointofInterestEntity);
patchDocument.ApplyTo(pointOfInterestToPatch, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!TryValidateModel(pointOfInterestToPatch))
{
return BadRequest(ModelState);
}
_mapper.Map(pointOfInterestToPatch, pointofInterestEntity);
await _cityInfoRepository.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{pointOfInterestId}")]
public async Task<ActionResult> DeletePointOfInterest(
int cityId,
int pointOfInterestId)
{
if (!await _cityInfoRepository.CityExistAsync(cityId))
{
return NotFound();
}
var pointofInterestEntity = await _cityInfoRepository.GetPointOfInterestForCityAsync(cityId, pointOfInterestId);
if (pointofInterestEntity == null)
{
return NotFound();
}
_cityInfoRepository.DeletePointOfInterest(pointofInterestEntity);
await _cityInfoRepository.SaveChangesAsync();
_mailService.Send("Point of interest deleted",
$"Point of Interest {pointofInterestEntity.Name} with Id {pointofInterestEntity.Id} was deleted.");
return NoContent();
}
}
}

View File

@@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace CityInfo.API.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@@ -0,0 +1,82 @@
using CityInfo.API.Entities;
using Microsoft.EntityFrameworkCore;
namespace CityInfo.API.DbContexts
{
public class CityInfoContext: DbContext
{
public DbSet<City> Cities { get; set; }
public DbSet<PointOfInterest> PointsOfInterest { get; set; }
//protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
//{
// optionsBuilder.UseSqlite("connectionstring");
// base.OnConfiguring(optionsBuilder);
//}
public CityInfoContext(DbContextOptions<CityInfoContext> options): base(options)
{}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<City>()
.HasData(
new City("New York City")
{
Id = 1,
Description = "The one with that big park."
},
new City("Antwerp")
{
Id = 2,
Description = "The one with the cathedral that was never really finished."
},
new City("Paris")
{
Id = 3,
Description = "The one with that big tower."
});
modelBuilder.Entity<PointOfInterest>()
.HasData(
new PointOfInterest("Central Park")
{
Id = 1,
CityId = 1,
Description = "The most visited urban park in the United States."
},
new PointOfInterest("Empire State Building")
{
Id = 2,
CityId = 1,
Description = "A 102-story skyscraper located in Midtown Manhattan."
},
new PointOfInterest("Cathedral")
{
Id = 3,
CityId = 2,
Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans."
},
new PointOfInterest("Antwerp Central Station")
{
Id = 4,
CityId = 2,
Description = "The the finest example of railway architecture in Belgium."
},
new PointOfInterest("Eiffel Tower")
{
Id = 5,
CityId = 3,
Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel."
},
new PointOfInterest("The Louvre")
{
Id = 6,
CityId = 3,
Description = "The world's largest museum."
}
);
base.OnModelCreating(modelBuilder);
}
}
}

View File

@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CityInfo.API.Entities
{
public class City
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(200)]
public string? Description { get; set; }
public ICollection<PointOfInterest> PointsOfInterest { get; set; } = new List<PointOfInterest>();
public City(string name)
{
Name = name;
}
}
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace CityInfo.API.Entities
{
public class PointOfInterest
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(200)]
public string? Description { get; set; }
[ForeignKey("CityId")]
public City? City { get; set; }
public int CityId { get; set; }
public PointOfInterest(string name)
{
Name = name;
}
}
}

View File

@@ -0,0 +1,81 @@
// <auto-generated />
using CityInfo.API.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CityInfo.API.Migrations
{
[DbContext(typeof(CityInfoContext))]
[Migration("20260310223100_CityInfoDBInitialMigration")]
partial class CityInfoDBInitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.4");
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Cities");
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CityId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("PointsOfInterest");
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.HasOne("CityInfo.API.Entities.City", "City")
.WithMany("PointsOfInterest")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Navigation("PointsOfInterest");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CityInfo.API.Migrations
{
/// <inheritdoc />
public partial class CityInfoDBInitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Cities",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Cities", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PointsOfInterest",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
CityId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PointsOfInterest", x => x.Id);
table.ForeignKey(
name: "FK_PointsOfInterest_Cities_CityId",
column: x => x.CityId,
principalTable: "Cities",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PointsOfInterest_CityId",
table: "PointsOfInterest",
column: "CityId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PointsOfInterest");
migrationBuilder.DropTable(
name: "Cities");
}
}
}

View File

@@ -0,0 +1,85 @@
// <auto-generated />
using CityInfo.API.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CityInfo.API.Migrations
{
[DbContext(typeof(CityInfoContext))]
[Migration("20260310223915_CityInfoDBAddPOIDescription")]
partial class CityInfoDBAddPOIDescription
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.4");
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Cities");
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CityId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("PointsOfInterest");
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.HasOne("CityInfo.API.Entities.City", "City")
.WithMany("PointsOfInterest")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Navigation("PointsOfInterest");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CityInfo.API.Migrations
{
/// <inheritdoc />
public partial class CityInfoDBAddPOIDescription : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
table: "PointsOfInterest",
type: "TEXT",
maxLength: 200,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "PointsOfInterest");
}
}
}

View File

@@ -0,0 +1,149 @@
// <auto-generated />
using CityInfo.API.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CityInfo.API.Migrations
{
[DbContext(typeof(CityInfoContext))]
[Migration("20260310224446_CityInfoInitialDataSeed")]
partial class CityInfoInitialDataSeed
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.4");
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Cities");
b.HasData(
new
{
Id = 1,
Description = "The one with that big park.",
Name = "New York City"
},
new
{
Id = 2,
Description = "The one with the cathedral that was never really finished.",
Name = "Antwerp"
},
new
{
Id = 3,
Description = "The one with that big tower.",
Name = "Paris"
});
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CityId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("PointsOfInterest");
b.HasData(
new
{
Id = 1,
CityId = 1,
Description = "The most visited urban park in the United States.",
Name = "Central Park"
},
new
{
Id = 2,
CityId = 1,
Description = "A 102-story skyscraper located in Midtown Manhattan.",
Name = "Empire State Building"
},
new
{
Id = 3,
CityId = 2,
Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.",
Name = "Cathedral"
},
new
{
Id = 4,
CityId = 2,
Description = "The the finest example of railway architecture in Belgium.",
Name = "Antwerp Central Station"
},
new
{
Id = 5,
CityId = 3,
Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.",
Name = "Eiffel Tower"
},
new
{
Id = 6,
CityId = 3,
Description = "The world's largest museum.",
Name = "The Louvre"
});
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.HasOne("CityInfo.API.Entities.City", "City")
.WithMany("PointsOfInterest")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Navigation("PointsOfInterest");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace CityInfo.API.Migrations
{
/// <inheritdoc />
public partial class CityInfoInitialDataSeed : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "Cities",
columns: new[] { "Id", "Description", "Name" },
values: new object[,]
{
{ 1, "The one with that big park.", "New York City" },
{ 2, "The one with the cathedral that was never really finished.", "Antwerp" },
{ 3, "The one with that big tower.", "Paris" }
});
migrationBuilder.InsertData(
table: "PointsOfInterest",
columns: new[] { "Id", "CityId", "Description", "Name" },
values: new object[,]
{
{ 1, 1, "The most visited urban park in the United States.", "Central Park" },
{ 2, 1, "A 102-story skyscraper located in Midtown Manhattan.", "Empire State Building" },
{ 3, 2, "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.", "Cathedral" },
{ 4, 2, "The the finest example of railway architecture in Belgium.", "Antwerp Central Station" },
{ 5, 3, "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.", "Eiffel Tower" },
{ 6, 3, "The world's largest museum.", "The Louvre" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 1);
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 2);
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 3);
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 4);
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 5);
migrationBuilder.DeleteData(
table: "PointsOfInterest",
keyColumn: "Id",
keyValue: 6);
migrationBuilder.DeleteData(
table: "Cities",
keyColumn: "Id",
keyValue: 1);
migrationBuilder.DeleteData(
table: "Cities",
keyColumn: "Id",
keyValue: 2);
migrationBuilder.DeleteData(
table: "Cities",
keyColumn: "Id",
keyValue: 3);
}
}
}

View File

@@ -0,0 +1,146 @@
// <auto-generated />
using CityInfo.API.DbContexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CityInfo.API.Migrations
{
[DbContext(typeof(CityInfoContext))]
partial class CityInfoContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.4");
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Cities");
b.HasData(
new
{
Id = 1,
Description = "The one with that big park.",
Name = "New York City"
},
new
{
Id = 2,
Description = "The one with the cathedral that was never really finished.",
Name = "Antwerp"
},
new
{
Id = 3,
Description = "The one with that big tower.",
Name = "Paris"
});
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CityId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CityId");
b.ToTable("PointsOfInterest");
b.HasData(
new
{
Id = 1,
CityId = 1,
Description = "The most visited urban park in the United States.",
Name = "Central Park"
},
new
{
Id = 2,
CityId = 1,
Description = "A 102-story skyscraper located in Midtown Manhattan.",
Name = "Empire State Building"
},
new
{
Id = 3,
CityId = 2,
Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans.",
Name = "Cathedral"
},
new
{
Id = 4,
CityId = 2,
Description = "The the finest example of railway architecture in Belgium.",
Name = "Antwerp Central Station"
},
new
{
Id = 5,
CityId = 3,
Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel.",
Name = "Eiffel Tower"
},
new
{
Id = 6,
CityId = 3,
Description = "The world's largest museum.",
Name = "The Louvre"
});
});
modelBuilder.Entity("CityInfo.API.Entities.PointOfInterest", b =>
{
b.HasOne("CityInfo.API.Entities.City", "City")
.WithMany("PointsOfInterest")
.HasForeignKey("CityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("City");
});
modelBuilder.Entity("CityInfo.API.Entities.City", b =>
{
b.Navigation("PointsOfInterest");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,77 @@
namespace CityInfo.API.Models
{
public class CityDataStore
{
public List<CityDto> Cities { get; set; }
public CityDataStore()
{
Cities = new List<CityDto>()
{
new CityDto()
{
Id = 1,
Name = "New York City",
Description = "The one with that big park.",
PointsOfInterest = new List<PointOfInterestDto>()
{
new PointOfInterestDto()
{
Id = 1,
Name = "Central Park",
Description = "The most visited urban park in the United States."
},
new PointOfInterestDto()
{
Id = 2,
Name = "Empire State Building",
Description = "A 102 Story skyscrapper located in Midtown Manathan."
}
}
},
new CityDto()
{
Id = 2,
Name = "Antwerp",
Description = "The one with the cathedral that was never really finished.",
PointsOfInterest = new List<PointOfInterestDto>()
{
new PointOfInterestDto()
{
Id = 3,
Name = "Antwerp Cathedral",
Description = "a beautiful Cathedral"
},
new PointOfInterestDto()
{
Id = 4,
Name = "Antwerp central station",
Description = "A train station"
}
}
},
new CityDto()
{
Id = 3,
Name = "Paris",
Description = "The one with that big tower.",
PointsOfInterest = new List<PointOfInterestDto>()
{
new PointOfInterestDto()
{
Id = 5,
Name = "Eiffel Tower",
Description = "A big tower."
},
new PointOfInterestDto()
{
Id = 6,
Name = "The Louvre",
Description = "A Museum."
}
}
},
};
}
}
}

View File

@@ -0,0 +1,16 @@
namespace CityInfo.API.Models
{
public class CityDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public int NumberOfPointsOfInterest {
get
{
return PointsOfInterest.Count;
}
}
public ICollection<PointOfInterestDto> PointsOfInterest { get; set; } = new List<PointOfInterestDto>();
}
}

View File

@@ -0,0 +1,9 @@
namespace CityInfo.API.Models
{
public class CityWithoutPointsOfInterestDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace CityInfo.API.Models
{
public class PointOfInterestDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace CityInfo.API.Models
{
public class PointOfInterestForCreationDto
{
[Required(ErrorMessage = "You should provide a name value.")]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[MaxLength(200)]
public string? Description { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace CityInfo.API.Models
{
public class PointOfInterestForUpdateDto
{
[Required(ErrorMessage = "You should provide a name value.")]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[MaxLength(200)]
public string? Description { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using AutoMapper;
using CityInfo.API.Entities;
using CityInfo.API.Models;
namespace CityInfo.API.Profiles
{
public class CityProfile: Profile
{
public CityProfile()
{
CreateMap<City, CityWithoutPointsOfInterestDto>();
CreateMap<City, CityDto>();
}
}
}

View File

@@ -0,0 +1,17 @@
using AutoMapper;
using CityInfo.API.Entities;
using CityInfo.API.Models;
namespace CityInfo.API.Profiles
{
public class PointOfInterestProfile: Profile
{
public PointOfInterestProfile()
{
CreateMap<PointOfInterest, PointOfInterestDto>();
CreateMap<PointOfInterestForCreationDto, PointOfInterest>();
CreateMap<PointOfInterestForUpdateDto, PointOfInterest>();
CreateMap<PointOfInterest, PointOfInterestForUpdateDto>();
}
}
}

View File

@@ -1,13 +1,87 @@
var builder = WebApplication.CreateBuilder(args); using CityInfo.API.DbContexts;
using CityInfo.API.Models;
using CityInfo.API.Services;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/cityinfo.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
//builder.logging.clearproviders();
//builder.logging.addconsole();
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers((options) =>
{
options.ReturnHttpNotAcceptable = true;
})
.AddNewtonsoftJson()
.AddXmlDataContractSerializerFormatters();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Services.AddSingleton<FileExtensionContentTypeProvider>();
builder.Services.AddSingleton<CityDataStore>();
builder.Services.AddDbContext<CityInfoContext>(
(dbContextOptins) => dbContextOptins.UseSqlite(
builder.Configuration["ConnectionStrings:CityInfoDBConnectionString"]
)
);
builder.Services.AddScoped<ICityInfoRepository, CityInfoRepository>();
builder.Services.AddProblemDetails();
//builder.Services.AddProblemDetails((options) =>
// {
// options.CustomizeProblemDetails = ctx =>
// {
// ctx.ProblemDetails.Extensions.Add("additionalInfo", "Addional info example");
// ctx.ProblemDetails.Extensions.Add("server", Environment.MachineName);
// };
// });
#if DEBUG
builder.Services.AddTransient<IMailService, LocalMailService>();
#else
builder.Services.AddTransient<IMailService, CloudMailService>();
#endif
builder.Services.AddAutoMapper(cfg => { }, AppDomain.CurrentDomain.GetAssemblies());
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Authentication:Issuer"],
ValidAudience = builder.Configuration["Authentication:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(builder.Configuration["Authentication:SecretForKey"]))
};
}
);
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("MustBeFromCharleroi", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("city", "Charleroi");
});
});
var app = builder.Build(); var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
@@ -21,8 +95,15 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run(); app.Run();

View File

@@ -0,0 +1,101 @@
using CityInfo.API.DbContexts;
using CityInfo.API.Entities;
using Microsoft.EntityFrameworkCore;
namespace CityInfo.API.Services
{
public class CityInfoRepository : ICityInfoRepository
{
private CityInfoContext _context;
public CityInfoRepository(CityInfoContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(CityInfoContext));
}
public async Task<IEnumerable<City>> GetCitiesAsync()
{
return await _context.Cities.OrderBy((city) => city.Name).ToListAsync();
}
public async Task<(IEnumerable<City>, PaginationMetadata)> GetCitiesAsync(string? name, string? searchQuery, int pageNumber, int pageSize)
{
var collection = _context.Cities as IQueryable<City>;
if (!string.IsNullOrEmpty(name))
{
name = name.Trim();
collection = collection.Where((city) => city.Name == name);
}
if (!string.IsNullOrEmpty(searchQuery))
{
searchQuery = searchQuery.Trim();
collection = collection.Where(
(city) => (
city.Name.Contains(searchQuery) |
(city.Description != null && city.Description.Contains(searchQuery))
)
);
}
var totalItemCount = await collection.CountAsync();
var paginationMetaData = new PaginationMetadata(totalItemCount, pageSize, pageNumber);
var collectionToReturn = await collection
.OrderBy((city) => city.Name)
.Skip(pageSize * (pageNumber - 1))
.Take(pageSize)
.ToListAsync();
return (collectionToReturn, paginationMetaData);
}
public async Task<City?> GetCityAsync(int cityId, bool includePointsOfinterest)
{
if (includePointsOfinterest)
{
return await _context.Cities
.Include((city) => city.PointsOfInterest)
.Where((city) => city.Id == cityId)
.FirstOrDefaultAsync();
}
return await _context.Cities
.Where((city) => city.Id == cityId)
.FirstOrDefaultAsync();
}
public async Task<bool> CityExistAsync(int cityId)
{
return await _context.Cities.AnyAsync((city) => city.Id == cityId);
}
public async Task<IEnumerable<PointOfInterest>> GetPointsOfinterestForCityAsync(int cityId)
{
return await _context.PointsOfInterest.Where((poi) => poi.CityId == cityId).ToListAsync();
}
public async Task<PointOfInterest?> GetPointOfInterestForCityAsync(int cityId, int pointOfInterestId)
{
return await _context.PointsOfInterest.Where((poi) => poi.CityId == cityId && poi.Id == pointOfInterestId).FirstOrDefaultAsync();
}
public async Task CreatePointOfInterestForCityAsync(int cityId, PointOfInterest pointOfInterest)
{
var city = await GetCityAsync(cityId, includePointsOfinterest: false);
if (city != null)
{
city.PointsOfInterest.Add(pointOfInterest);
}
}
public async Task<bool> SaveChangesAsync()
{
return (await _context.SaveChangesAsync() >= 0);
}
public void DeletePointOfInterest(PointOfInterest pointOfInterest)
{
_context.PointsOfInterest.Remove(pointOfInterest);
}
public async Task<bool> CityNameMatchesCityId(string? cityName, int cityId)
{
return await _context.Cities.AnyAsync((city) => city.Name == cityName && city.Id == cityId);
}
}
}

View File

@@ -0,0 +1,20 @@
namespace CityInfo.API.Services
{
public class CloudMailService : IMailService
{
private string _mailTo = string.Empty;
private string _mailFrom = string.Empty;
public CloudMailService(
IConfiguration configuration)
{
_mailTo = configuration["mailsettings:mailToAddress"];
_mailFrom = configuration["mailsettings:mailFromAddress"];
}
public void Send(string subject, string message)
{
Console.WriteLine($"Mail from {_mailFrom} to {_mailTo} with {nameof(CloudMailService)}");
Console.WriteLine($"Subject: {subject}");
Console.WriteLine($"Message: {message}");
}
}
}

View File

@@ -0,0 +1,18 @@
using CityInfo.API.Entities;
namespace CityInfo.API.Services
{
public interface ICityInfoRepository
{
Task<IEnumerable<City>> GetCitiesAsync();
Task<(IEnumerable<City>, PaginationMetadata)> GetCitiesAsync(string? name, string? searchQuery, int pageNumber, int pageSize);
Task<City?> GetCityAsync(int cityId, bool includePointsOfinterest);
Task<bool> CityExistAsync(int cityId);
Task<IEnumerable<PointOfInterest>> GetPointsOfinterestForCityAsync(int cityId);
Task<PointOfInterest?> GetPointOfInterestForCityAsync(int cityId, int pointOfInterestId);
Task CreatePointOfInterestForCityAsync(int cityId, PointOfInterest pointOfInterest);
void DeletePointOfInterest(PointOfInterest pointOfInterest);
Task<bool> CityNameMatchesCityId(string? cityName, int cityId);
Task<bool> SaveChangesAsync();
}
}

View File

@@ -0,0 +1,7 @@
namespace CityInfo.API.Services
{
public interface IMailService
{
public void Send(string subject, string message);
}
}

View File

@@ -0,0 +1,21 @@
namespace CityInfo.API.Services
{
public class LocalMailService:IMailService
{
private string _mailTo = string.Empty;
private string _mailFrom = string.Empty;
public LocalMailService(
IConfiguration configuration)
{
_mailTo = configuration["mailsettings:mailToAddress"];
_mailFrom = configuration["mailsettings:mailFromAddress"];
}
public void Send(string subject, string message)
{
Console.WriteLine($"Mail from {_mailFrom} to {_mailTo} with {nameof(LocalMailService)}");
Console.WriteLine($"Subject: {subject}");
Console.WriteLine($"Message: {message}");
}
}
}

View File

@@ -0,0 +1,18 @@
namespace CityInfo.API.Services
{
public class PaginationMetadata
{
public int TotalItemCount { get; set; }
public int TotalPageCount { get; set; }
public int PageSize { get; set; }
public int CurrentPage { get; set; }
public PaginationMetadata(int totalItemCount, int pageSize, int currentPage)
{
TotalItemCount = totalItemCount;
PageSize = pageSize;
CurrentPage = currentPage;
TotalPageCount = (int)Math.Ceiling(totalItemCount / (double)pageSize);
}
}
}

View File

@@ -1,13 +0,0 @@
namespace CityInfo.API
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View File

@@ -2,7 +2,17 @@
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
} "CityInfo.API.Controllers": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
"ConnectionStrings": {
"CityInfoDBConnectionString": "Data Source=CityInfo.db"
},
"Authentication": {
"SecretForKey": "RgDldLrK+p+T0JisAKdD7THnT/npmWY14vV3UUiRSVE=",
"Issuer": "https://localhost:7289",
"Audience": "cityinfoapi"
} }
} }

View File

@@ -0,0 +1,5 @@
{
"mailSettings": {
"mailToAddress": "admin@mycompany.com",
}
}

View File

@@ -5,5 +5,9 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"mailSettings": {
"mailToAddress": "developers@mycompany.com",
"mailFromAddress": "noreply@mycompany.com"
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

File diff suppressed because it is too large Load Diff