From ce9579cb651d0b7c2dc8be4b34c6450385bbfcf6 Mon Sep 17 00:00:00 2001 From: Andrey Kotov Date: Fri, 1 Mar 2024 21:10:34 +0400 Subject: [PATCH 1/3] load heroes from api --- .gitignore | 6 ++++ ClientApp/package-lock.json | 9 ++++-- ClientApp/src/app/app.module.ts | 12 +++---- .../app/fetch-data/fetch-data.component.html | 24 -------------- .../app/fetch-data/fetch-data.component.ts | 23 -------------- ClientApp/src/app/home/hero.ts | 7 +++++ ClientApp/src/app/home/heroes.service.ts | 19 ++++++++++++ ClientApp/src/app/home/home.component.html | 16 +++++----- ClientApp/src/app/home/home.component.ts | 15 +++++++++ Controllers/HeroesController.cs | 22 +++++-------- Controllers/WeatherForecastController.cs | 31 ------------------- Dto/HeroDto.cs | 3 ++ Program.cs | 4 ++- Services/HeroService.cs | 27 ++++++++++++++++ WeatherForecast.cs | 12 ------- 15 files changed, 106 insertions(+), 124 deletions(-) delete mode 100644 ClientApp/src/app/fetch-data/fetch-data.component.html delete mode 100644 ClientApp/src/app/fetch-data/fetch-data.component.ts create mode 100644 ClientApp/src/app/home/hero.ts create mode 100644 ClientApp/src/app/home/heroes.service.ts delete mode 100644 Controllers/WeatherForecastController.cs create mode 100644 Dto/HeroDto.cs create mode 100644 Services/HeroService.cs delete mode 100644 WeatherForecast.cs diff --git a/.gitignore b/.gitignore index 41ffa34..941ad8c 100644 --- a/.gitignore +++ b/.gitignore @@ -229,3 +229,9 @@ _Pvt_Extensions # FAKE - F# Make .fake/ + +# Rider +.idea/ + +# Angular +.angular/ \ No newline at end of file diff --git a/ClientApp/package-lock.json b/ClientApp/package-lock.json index 446ad8f..e87908a 100644 --- a/ClientApp/package-lock.json +++ b/ClientApp/package-lock.json @@ -11391,7 +11391,7 @@ "@discoveryjs/json-ext": "0.5.7", "@ngtools/webpack": "15.1.6", "ansi-colors": "4.1.3", - "autoprefixer": "10.4.13", + "autoprefixer": "10.4.5", "babel-loader": "9.1.2", "babel-plugin-istanbul": "6.1.1", "browserslist": "4.21.4", @@ -13814,7 +13814,9 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "requires": {} + "requires": { + "ajv": "^8.0.0" + } }, "ajv-keywords": { "version": "5.1.0", @@ -13903,7 +13905,8 @@ "dev": true }, "autoprefixer": { - "version": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.5.tgz", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.5.tgz", "integrity": "sha512-Fvd8yCoA7lNX/OUllvS+aS1I7WRBclGXsepbvT8ZaPgrH24rgXpZzF0/6Hh3ZEkwg+0AES/Osd196VZmYoEFtw==", "dev": true, "requires": { diff --git a/ClientApp/src/app/app.module.ts b/ClientApp/src/app/app.module.ts index 64754de..a7e0e3a 100644 --- a/ClientApp/src/app/app.module.ts +++ b/ClientApp/src/app/app.module.ts @@ -8,26 +8,24 @@ import { AppComponent } from './app.component'; import { NavMenuComponent } from './nav-menu/nav-menu.component'; import { HomeComponent } from './home/home.component'; import { CounterComponent } from './counter/counter.component'; -import { FetchDataComponent } from './fetch-data/fetch-data.component'; +import {HeroesService} from "./home/heroes.service"; @NgModule({ - declarations: [ + declarations: [ AppComponent, NavMenuComponent, HomeComponent, - CounterComponent, - FetchDataComponent, + CounterComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), HttpClientModule, FormsModule, RouterModule.forRoot([ - { path: '', component: HomeComponent, pathMatch: 'full' }, - { path: 'test', component: FetchDataComponent }, + { path: '', component: HomeComponent, pathMatch: 'full' } ]) ], - providers: [], + providers: [HeroesService], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/ClientApp/src/app/fetch-data/fetch-data.component.html b/ClientApp/src/app/fetch-data/fetch-data.component.html deleted file mode 100644 index 19f8aae..0000000 --- a/ClientApp/src/app/fetch-data/fetch-data.component.html +++ /dev/null @@ -1,24 +0,0 @@ -

Weather forecast

- -

This component demonstrates fetching data from the server.

- -

Loading...

- - - - - - - - - - - - - - - - - - -
DateTemp. (C)Temp. (F)Summary
{{ forecast.Date }}{{ forecast.TemperatureC }}{{ forecast.TemperatureF }}{{ forecast.Summary }}
diff --git a/ClientApp/src/app/fetch-data/fetch-data.component.ts b/ClientApp/src/app/fetch-data/fetch-data.component.ts deleted file mode 100644 index 4ed26bc..0000000 --- a/ClientApp/src/app/fetch-data/fetch-data.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; - -@Component({ - selector: 'app-fetch-data', - templateUrl: './fetch-data.component.html' -}) -export class FetchDataComponent { - public forecasts: WeatherForecast[] = []; - - constructor(http: HttpClient, @Inject('BASE_URL') baseUrl: string) { - http.get(baseUrl + 'heroes').subscribe(result => { - this.forecasts = result; - }, error => console.error(error)); - } -} - -interface WeatherForecast { - Date: string; - TemperatureC: number; - TemperatureF: number; - Summary: string; -} diff --git a/ClientApp/src/app/home/hero.ts b/ClientApp/src/app/home/hero.ts new file mode 100644 index 0000000..66971c3 --- /dev/null +++ b/ClientApp/src/app/home/hero.ts @@ -0,0 +1,7 @@ +export interface Hero { + Id: number; + Name: string; + Alias?: string; + BrandId: number; + BrandName: string; +} diff --git a/ClientApp/src/app/home/heroes.service.ts b/ClientApp/src/app/home/heroes.service.ts new file mode 100644 index 0000000..5cccdb2 --- /dev/null +++ b/ClientApp/src/app/home/heroes.service.ts @@ -0,0 +1,19 @@ +import {Inject, Injectable} from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable} from 'rxjs'; + +import { Hero } from './hero'; + + +@Injectable() +export class HeroesService { + + constructor( + private http: HttpClient, + @Inject('BASE_URL') private baseUrl: string) { + } + + get(): Observable { + return this.http.get(this.baseUrl + 'heroes'); + } +} diff --git a/ClientApp/src/app/home/home.component.html b/ClientApp/src/app/home/home.component.html index 54ee5f2..48e10e9 100644 --- a/ClientApp/src/app/home/home.component.html +++ b/ClientApp/src/app/home/home.component.html @@ -1,10 +1,8 @@

Populate Table With List of Heroes

-

Delete Example data and make list dynamic using a angular service file that calls an api on HeroesController to get the heroes from db

-

Also make the Add Hero link in nav go to a new page with a form that will add a new Hero to Heroes table in db and then redirect back to this page. Do not worry about validating duplicate hero names

-

The delete button should flag the hero in the db as IsActive = 0

+

Loading...

- +
@@ -15,11 +13,11 @@

Populate Table With List of Heroes

- - - - - + + + + + diff --git a/ClientApp/src/app/home/home.component.ts b/ClientApp/src/app/home/home.component.ts index 2747b30..cd1cd89 100644 --- a/ClientApp/src/app/home/home.component.ts +++ b/ClientApp/src/app/home/home.component.ts @@ -1,8 +1,23 @@ import { Component } from '@angular/core'; +import {HeroesService} from "./heroes.service"; +import {Hero} from "./hero"; @Component({ selector: 'app-home', templateUrl: './home.component.html', + providers: [ HeroesService ], }) export class HomeComponent { + public heroes: Hero[] = []; + + constructor( + private service: HeroesService) { + } + + ngOnInit() { + this.service.get().subscribe((heroes: Hero[]) => { + this.heroes = heroes; + }); + } + } diff --git a/Controllers/HeroesController.cs b/Controllers/HeroesController.cs index b7f7248..7becd5a 100644 --- a/Controllers/HeroesController.cs +++ b/Controllers/HeroesController.cs @@ -1,32 +1,26 @@ -using Microsoft.AspNetCore.Mvc; +using HeroTest.Services; +using Microsoft.AspNetCore.Mvc; namespace HeroTest.Controllers; [ApiController] [Route("[controller]")] public class HeroesController : ControllerBase { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; private readonly ILogger _logger; + private readonly IHeroService _heroService; - public HeroesController(ILogger logger) + public HeroesController(ILogger logger, IHeroService heroService) { _logger = logger; + _heroService = heroService; } [HttpGet] - public IEnumerable Get() + public async Task Get() { - return Enumerable.Range(1, 500).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); + var heroes = await _heroService.Get(); + return Ok(heroes); } } diff --git a/Controllers/WeatherForecastController.cs b/Controllers/WeatherForecastController.cs deleted file mode 100644 index 966f939..0000000 --- a/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace HeroTest.Controllers; -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} diff --git a/Dto/HeroDto.cs b/Dto/HeroDto.cs new file mode 100644 index 0000000..70db469 --- /dev/null +++ b/Dto/HeroDto.cs @@ -0,0 +1,3 @@ +namespace HeroTest.Dto; + +public record HeroDto(int Id, string Name, string? Alias, DateTime CreatedOn, DateTime UpdatedOn, int BrandId, string BrandName); \ No newline at end of file diff --git a/Program.cs b/Program.cs index 190f5c0..8e3ab0f 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,5 @@ using HeroTest.Models; +using HeroTest.Services; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -10,8 +11,9 @@ builder.Services.AddDbContext(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("ObjectDB")); - }); +builder.Services + .AddScoped(); builder.Services.AddControllers() .AddJsonOptions(options => diff --git a/Services/HeroService.cs b/Services/HeroService.cs new file mode 100644 index 0000000..2c410cb --- /dev/null +++ b/Services/HeroService.cs @@ -0,0 +1,27 @@ +using HeroTest.Dto; +using HeroTest.Models; +using Microsoft.EntityFrameworkCore; + +namespace HeroTest.Services; + +public interface IHeroService +{ + Task> Get(); +} + +public class HeroService : IHeroService +{ + private readonly SampleContext _context; + + public HeroService(SampleContext context) + { + _context = context; + } + public async Task> Get() + { + return await _context.Heroes.Include(h => h.Brand) + .Where(h => h.IsActive != false) + .Select(h => new HeroDto(h.Id, h.Name, h.Alias, h.CreatedOn, h.UpdatedOn, h.BrandId, h.Brand.Name)) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/WeatherForecast.cs b/WeatherForecast.cs deleted file mode 100644 index 55a88b2..0000000 --- a/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace HeroTest; - -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; } -} From ba4d23311bf6a63609354436acbd3615aa46dd4d Mon Sep 17 00:00:00 2001 From: Andrey Kotov Date: Sat, 2 Mar 2024 00:43:08 +0400 Subject: [PATCH 2/3] add, delete heroes --- .../src/app/add-hero/add-hero.component.css | 9 +++ .../src/app/add-hero/add-hero.component.html | 13 +++++ .../src/app/add-hero/add-hero.component.ts | 33 +++++++++++ ClientApp/src/app/add-hero/add-hero.ts | 5 ++ ClientApp/src/app/app.module.ts | 21 ++++--- .../src/app/counter/counter.component.html | 7 --- .../src/app/counter/counter.component.spec.ts | 34 ------------ .../src/app/counter/counter.component.ts | 13 ----- ClientApp/src/app/{home => heroes}/hero.ts | 0 .../heroes.component.html} | 4 +- .../heroes.component.ts} | 16 ++++-- .../app/{home => heroes}/heroes.service.ts | 11 +++- Controllers/HeroesController.cs | 32 +++++++++-- HeroTest.csproj | 12 ++-- Models/Brand.cs | 2 +- Models/SampleContext.cs | 2 + Services/HeroService.cs | 55 ++++++++++++++++++- ViewModels/AddHeroModel.cs | 15 +++++ 18 files changed, 201 insertions(+), 83 deletions(-) create mode 100644 ClientApp/src/app/add-hero/add-hero.component.css create mode 100644 ClientApp/src/app/add-hero/add-hero.component.html create mode 100644 ClientApp/src/app/add-hero/add-hero.component.ts create mode 100644 ClientApp/src/app/add-hero/add-hero.ts delete mode 100644 ClientApp/src/app/counter/counter.component.html delete mode 100644 ClientApp/src/app/counter/counter.component.spec.ts delete mode 100644 ClientApp/src/app/counter/counter.component.ts rename ClientApp/src/app/{home => heroes}/hero.ts (100%) rename ClientApp/src/app/{home/home.component.html => heroes/heroes.component.html} (80%) rename ClientApp/src/app/{home/home.component.ts => heroes/heroes.component.ts} (56%) rename ClientApp/src/app/{home => heroes}/heroes.service.ts (52%) create mode 100644 ViewModels/AddHeroModel.cs diff --git a/ClientApp/src/app/add-hero/add-hero.component.css b/ClientApp/src/app/add-hero/add-hero.component.css new file mode 100644 index 0000000..f55e5a2 --- /dev/null +++ b/ClientApp/src/app/add-hero/add-hero.component.css @@ -0,0 +1,9 @@ +form { + padding-top: 1rem; +} + +label { + display: block; + margin: .5em 0; + font-weight: bold; +} diff --git a/ClientApp/src/app/add-hero/add-hero.component.html b/ClientApp/src/app/add-hero/add-hero.component.html new file mode 100644 index 0000000..1c6cf2a --- /dev/null +++ b/ClientApp/src/app/add-hero/add-hero.component.html @@ -0,0 +1,13 @@ +
+ + + + + + + + + +

Complete the form to enable button.

+ + diff --git a/ClientApp/src/app/add-hero/add-hero.component.ts b/ClientApp/src/app/add-hero/add-hero.component.ts new file mode 100644 index 0000000..9b80c44 --- /dev/null +++ b/ClientApp/src/app/add-hero/add-hero.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import {FormBuilder, Validators} from "@angular/forms"; +import {HeroesService} from "../heroes/heroes.service"; +import {AddHero} from "./add-hero"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-hero-component', + templateUrl: './add-hero.component.html', + styleUrls: ['./add-hero.component.css'] +}) +export class AddHeroComponent { + errors: any = []; + + heroForm = this.formBuilder.group({ + name: ['', [Validators.required, Validators.maxLength(100)]], + alias: ['', [Validators.required, Validators.maxLength(50)]], + brand: ['', [Validators.required, Validators.maxLength(100)]], + }); + constructor(private formBuilder: FormBuilder, + private service: HeroesService, + private router: Router) {} + + onSubmit() { + const hero = this.heroForm.value as AddHero; + this.service.create(hero).subscribe({ + next: _ => this.router.navigate(['/']), + error: error => { + this.errors = error; + } + }) + } +} diff --git a/ClientApp/src/app/add-hero/add-hero.ts b/ClientApp/src/app/add-hero/add-hero.ts new file mode 100644 index 0000000..76f5343 --- /dev/null +++ b/ClientApp/src/app/add-hero/add-hero.ts @@ -0,0 +1,5 @@ +export interface AddHero { + name: string; + alias: string; + brand: string; +} diff --git a/ClientApp/src/app/app.module.ts b/ClientApp/src/app/app.module.ts index a7e0e3a..7b021be 100644 --- a/ClientApp/src/app/app.module.ts +++ b/ClientApp/src/app/app.module.ts @@ -1,28 +1,33 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { NavMenuComponent } from './nav-menu/nav-menu.component'; -import { HomeComponent } from './home/home.component'; -import { CounterComponent } from './counter/counter.component'; -import {HeroesService} from "./home/heroes.service"; +import { HeroesComponent } from './heroes/heroes.component'; +import { AddHeroComponent } from './add-hero/add-hero.component'; +import { HeroesService } from "./heroes/heroes.service"; +import {JsonPipe, NgFor} from "@angular/common"; @NgModule({ declarations: [ AppComponent, NavMenuComponent, - HomeComponent, - CounterComponent + HeroesComponent, + AddHeroComponent ], imports: [ - BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), + BrowserModule.withServerTransition({ appId: 'ng-cli-uni8ersal' }), HttpClientModule, + ReactiveFormsModule, FormsModule, + JsonPipe, + NgFor, RouterModule.forRoot([ - { path: '', component: HomeComponent, pathMatch: 'full' } + { path: '', component: HeroesComponent, pathMatch: 'full' }, + { path: 'add-hero', component: AddHeroComponent, pathMatch: 'full' } ]) ], providers: [HeroesService], diff --git a/ClientApp/src/app/counter/counter.component.html b/ClientApp/src/app/counter/counter.component.html deleted file mode 100644 index 89b9c80..0000000 --- a/ClientApp/src/app/counter/counter.component.html +++ /dev/null @@ -1,7 +0,0 @@ -

Counter

- -

This is a simple example of an Angular component.

- -

Current count: {{ currentCount }}

- - diff --git a/ClientApp/src/app/counter/counter.component.spec.ts b/ClientApp/src/app/counter/counter.component.spec.ts deleted file mode 100644 index 37b350c..0000000 --- a/ClientApp/src/app/counter/counter.component.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CounterComponent } from './counter.component'; - -describe('CounterComponent', () => { - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ CounterComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CounterComponent); - fixture.detectChanges(); - }); - - it('should display a title', async(() => { - const titleText = fixture.nativeElement.querySelector('h1').textContent; - expect(titleText).toEqual('Counter'); - })); - - it('should start with count 0, then increments by 1 when clicked', async(() => { - const countElement = fixture.nativeElement.querySelector('strong'); - expect(countElement.textContent).toEqual('0'); - - const incrementButton = fixture.nativeElement.querySelector('button'); - incrementButton.click(); - fixture.detectChanges(); - expect(countElement.textContent).toEqual('1'); - })); -}); diff --git a/ClientApp/src/app/counter/counter.component.ts b/ClientApp/src/app/counter/counter.component.ts deleted file mode 100644 index 1f336aa..0000000 --- a/ClientApp/src/app/counter/counter.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-counter-component', - templateUrl: './counter.component.html' -}) -export class CounterComponent { - public currentCount = 0; - - public incrementCounter() { - this.currentCount++; - } -} diff --git a/ClientApp/src/app/home/hero.ts b/ClientApp/src/app/heroes/hero.ts similarity index 100% rename from ClientApp/src/app/home/hero.ts rename to ClientApp/src/app/heroes/hero.ts diff --git a/ClientApp/src/app/home/home.component.html b/ClientApp/src/app/heroes/heroes.component.html similarity index 80% rename from ClientApp/src/app/home/home.component.html rename to ClientApp/src/app/heroes/heroes.component.html index 48e10e9..1dfb053 100644 --- a/ClientApp/src/app/home/home.component.html +++ b/ClientApp/src/app/heroes/heroes.component.html @@ -1,4 +1,4 @@ -

Populate Table With List of Heroes

+

Heroes

Loading...

@@ -19,7 +19,7 @@

Populate Table With List of Heroes

diff --git a/ClientApp/src/app/home/home.component.ts b/ClientApp/src/app/heroes/heroes.component.ts similarity index 56% rename from ClientApp/src/app/home/home.component.ts rename to ClientApp/src/app/heroes/heroes.component.ts index cd1cd89..4e6bf39 100644 --- a/ClientApp/src/app/home/home.component.ts +++ b/ClientApp/src/app/heroes/heroes.component.ts @@ -3,11 +3,11 @@ import {HeroesService} from "./heroes.service"; import {Hero} from "./hero"; @Component({ - selector: 'app-home', - templateUrl: './home.component.html', + selector: 'app-heroes', + templateUrl: './heroes.component.html', providers: [ HeroesService ], }) -export class HomeComponent { +export class HeroesComponent { public heroes: Hero[] = []; constructor( @@ -15,9 +15,17 @@ export class HomeComponent { } ngOnInit() { + this.load(); + } + + load() { this.service.get().subscribe((heroes: Hero[]) => { this.heroes = heroes; }); } - + delete(hero: Hero){ + if(confirm(`Are you sure to delete ${hero.Name}?`)) { + this.service.delete(hero.Id).subscribe(_ => this.load()); + } + } } diff --git a/ClientApp/src/app/home/heroes.service.ts b/ClientApp/src/app/heroes/heroes.service.ts similarity index 52% rename from ClientApp/src/app/home/heroes.service.ts rename to ClientApp/src/app/heroes/heroes.service.ts index 5cccdb2..0b73905 100644 --- a/ClientApp/src/app/home/heroes.service.ts +++ b/ClientApp/src/app/heroes/heroes.service.ts @@ -1,8 +1,9 @@ -import {Inject, Injectable} from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable} from 'rxjs'; import { Hero } from './hero'; +import { AddHero } from "../add-hero/add-hero"; @Injectable() @@ -16,4 +17,12 @@ export class HeroesService { get(): Observable { return this.http.get(this.baseUrl + 'heroes'); } + + delete(id: number): Observable { + return this.http.delete(`${this.baseUrl}heroes/${id}`); + } + + create(hero: AddHero): Observable { + return this.http.post(this.baseUrl + 'heroes', hero); + } } diff --git a/Controllers/HeroesController.cs b/Controllers/HeroesController.cs index 7becd5a..611544f 100644 --- a/Controllers/HeroesController.cs +++ b/Controllers/HeroesController.cs @@ -1,18 +1,17 @@ using HeroTest.Services; +using HeroTest.ViewModels; using Microsoft.AspNetCore.Mvc; namespace HeroTest.Controllers; + [ApiController] [Route("[controller]")] public class HeroesController : ControllerBase { - - private readonly ILogger _logger; private readonly IHeroService _heroService; - public HeroesController(ILogger logger, IHeroService heroService) + public HeroesController(IHeroService heroService) { - _logger = logger; _heroService = heroService; } @@ -22,5 +21,30 @@ public async Task Get() var heroes = await _heroService.Get(); return Ok(heroes); } + + [HttpGet("{heroId:int}")] + public async Task Get(int heroId) + { + var hero = await _heroService.Get(heroId); + return Ok(hero); + } + + [HttpPost] + public async Task Create([FromBody] AddHeroModel model) + { + if (!ModelState.IsValid) + return BadRequest(); + var hero = await _heroService.Create(model); + return Created(Url.Action(nameof(Get), new {heroId = hero.Id})!, hero); + } + + [HttpDelete("{heroId:int}")] + public async Task Delete(int heroId) + { + var deleted = await _heroService.Delete(heroId); + if (deleted) + return Ok(); + return NotFound(); + } } diff --git a/HeroTest.csproj b/HeroTest.csproj index d87a58a..d418c53 100644 --- a/HeroTest.csproj +++ b/HeroTest.csproj @@ -11,10 +11,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,10 +27,6 @@ - - - - diff --git a/Models/Brand.cs b/Models/Brand.cs index efd1f92..212571b 100644 --- a/Models/Brand.cs +++ b/Models/Brand.cs @@ -7,7 +7,7 @@ public Brand() Heroes = new HashSet(); } - public int Id { get; set; } = 1; + public int Id { get; set; } public string Name { get; set; } = null!; public bool? IsActive { get; set; } public DateTime CreatedOn { get; set; } diff --git a/Models/SampleContext.cs b/Models/SampleContext.cs index 87e90ef..f1277d8 100644 --- a/Models/SampleContext.cs +++ b/Models/SampleContext.cs @@ -29,6 +29,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.ToTable("Brand"); + entity.Property(e => e.Id).UseIdentityColumn(); entity.Property(e => e.CreatedOn).HasDefaultValueSql("(getutcdate())"); @@ -44,6 +45,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.Property(e => e.Alias).HasMaxLength(50); + entity.Property(e => e.Id).UseIdentityColumn(); entity.Property(e => e.BrandId).HasDefaultValueSql("((1))"); diff --git a/Services/HeroService.cs b/Services/HeroService.cs index 2c410cb..6f7620c 100644 --- a/Services/HeroService.cs +++ b/Services/HeroService.cs @@ -1,5 +1,6 @@ using HeroTest.Dto; using HeroTest.Models; +using HeroTest.ViewModels; using Microsoft.EntityFrameworkCore; namespace HeroTest.Services; @@ -7,6 +8,9 @@ namespace HeroTest.Services; public interface IHeroService { Task> Get(); + Task Get(int heroId); + Task Create(AddHeroModel model); + Task Delete(int heroId); } public class HeroService : IHeroService @@ -19,9 +23,58 @@ public HeroService(SampleContext context) } public async Task> Get() { - return await _context.Heroes.Include(h => h.Brand) + return await _context.Heroes + .AsNoTracking() + .Include(h => h.Brand) .Where(h => h.IsActive != false) .Select(h => new HeroDto(h.Id, h.Name, h.Alias, h.CreatedOn, h.UpdatedOn, h.BrandId, h.Brand.Name)) .ToListAsync(); } + + public async Task Get(int heroId) + { + return await _context.Heroes + .AsNoTracking() + .Include(h => h.Brand) + .Where(h => h.IsActive != false && h.Id == heroId) + .Select(h => new HeroDto(h.Id, h.Name, h.Alias, h.CreatedOn, h.UpdatedOn, h.BrandId, h.Brand.Name)) + .SingleOrDefaultAsync(); + } + + public async Task Create(AddHeroModel model) + { + var brandName = model.Brand.Trim(); + var brand = await _context.Brands.SingleOrDefaultAsync(b => b.Name == brandName); + if (brand == default) + { + brand = new Brand() + { + Name = brandName + }; + await _context.Brands.AddAsync(brand); + } + + var hero = new Hero() + { + Name = model.Name, + Alias = model.Alias, + BrandId = brand.Id, + Brand = brand + }; + await _context.Heroes.AddAsync(hero); + await _context.SaveChangesAsync(); + return new HeroDto(hero.Id, hero.Name, hero.Alias, hero.CreatedOn, hero.UpdatedOn, hero.BrandId, + hero.Brand.Name); + } + + public async Task Delete(int heroId) + { + var hero = await _context.Heroes.SingleOrDefaultAsync(h => h.Id == heroId); + if (hero == default) + return false; + hero.IsActive = false; + hero.UpdatedOn = DateTime.UtcNow; + await _context.SaveChangesAsync(); + return true; + } } \ No newline at end of file diff --git a/ViewModels/AddHeroModel.cs b/ViewModels/AddHeroModel.cs new file mode 100644 index 0000000..478d8d6 --- /dev/null +++ b/ViewModels/AddHeroModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace HeroTest.ViewModels; + +public record AddHeroModel +{ + [Required, MaxLength(100)] + public string Name { get; set; } = null!; + + [Required, MaxLength(50)] + public string Alias { get; set; } = null!; + + [Required, MaxLength(100)] + public string Brand { get; set; } = null!; +} \ No newline at end of file From ce5307eea289c256993cddac8289a89be76673a7 Mon Sep 17 00:00:00 2001 From: Andrey Kotov Date: Sat, 2 Mar 2024 16:05:48 +0400 Subject: [PATCH 3/3] show validation errors --- .../src/app/add-hero/add-hero.component.html | 60 ++++++++++++++++--- .../src/app/add-hero/add-hero.component.ts | 12 ++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/ClientApp/src/app/add-hero/add-hero.component.html b/ClientApp/src/app/add-hero/add-hero.component.html index 1c6cf2a..25fe10c 100644 --- a/ClientApp/src/app/add-hero/add-hero.component.html +++ b/ClientApp/src/app/add-hero/add-hero.component.html @@ -1,13 +1,55 @@
- - - - - - - - - +
+
+ + +
+
+ This field is required. +
+
+ This field must have at most 100 characters. +
+
+ {{errors.name}} +
+
+
+
+
+
+ + +
+
+ This field is required. +
+
+ This field must have at most 50 characters. +
+
+ {{errors.alias}} +
+
+
+
+
+
+ + +
+
+ This field is required. +
+
+ This field must have at most 100 characters. +
+
+ {{errors.brand}} +
+
+
+

Complete the form to enable button.

diff --git a/ClientApp/src/app/add-hero/add-hero.component.ts b/ClientApp/src/app/add-hero/add-hero.component.ts index 9b80c44..9a9408f 100644 --- a/ClientApp/src/app/add-hero/add-hero.component.ts +++ b/ClientApp/src/app/add-hero/add-hero.component.ts @@ -30,4 +30,16 @@ export class AddHeroComponent { } }) } + + get name() { + return this.heroForm.get('name')!; + } + + get alias() { + return this.heroForm.get('alias')!; + } + + get brand() { + return this.heroForm.get('brand')!; + } }
Id
1SupermanClark KentDC
{{ hero.Id }}{{ hero.Name }}{{ hero.Alias }}{{ hero.BrandName }} {{ hero.Alias }} {{ hero.BrandName }} - +