diff --git a/.gitignore b/.gitignore index 9491a2f..158624b 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,15 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +*/**/bin/Debug +*/**/bin/Release +*/**/obj/Debug +*/**/obj/Release +*/Migrations +/workshop.wwwapi/appsettings.json +/workshop.wwwapi/appsettings.Development.json + +appsettings.json +appsettings.Development.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa062f3 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# C# Entity Framework Intro + +1. Fork this repository +2. Clone your fork to your machine +3. Open the ef.intro.sln in Visual Studio + +## Setup + + + +- Note the .gitignore file in the root of the project which prevents the build directories being uploaded: +``` +*/**/bin/Debug +*/**/bin/Release +*/**/obj/Debug +*/Migrations +*/**/obj/Release +/workshop.wwwapi/appsettings.json +/workshop.wwwapi/appsettings.Development.json +``` + + +## Dependencies + +Some of these have already been installed: + +- Install-Package Scalar.AspNetCore + - provides a /scalar endpoint +- Install-Package Swashbuckle.AspNetCore + - provides a /swagger endpoint + +- Install-Package Microsoft.EntityFrameworkCore +- Install-Package Microsoft.EntityFrameworkCore.Design +- Install-Package Microsoft.EntityFrameworkCore.InMemory + +## Core and Extension are combined into the following requirements + +The overall objective is to complete the BookAPI CRUD operations, using DTO objects to return nicely formatted json without any cyclical serialization errors. + +As guidelines we suggest: + +- implement the GET book and GET all books. When you return the books objects, use an appropriate DTO to return the book + author (but no nested books inside author). Make sure to include the authors when you load the data in the repository. +- implement the UPDATE boook where you can change the author via id (you may skip updating other properties like title, etc); make sure to return the Book + Author once the update is done +- implement the DELETE book +- implement the CREATE book - it should return NotFound when author id is not valid and BadRequest when book object not valid + +- implement the author API (interested in just the GET, GET all) -> the author should return the list of books, use its own author response DTO + + +Extensions (each one is one extension, implement at least one): + +- Add a publisher model and add that as an additional relation to the book, where a book has one publisher; make sure to update the seeder and to create a publisher API Endpoints with the GET and GET all endpoints. Getting a Publisher should return all books that have that publisher + the author for each book. Update the author endpoint to return the Book + Publisher; Updaate the book endpoint to return the Book + Author + Publisher. +- Update the model to have many to many relation between Book and Author, where a Book can have 1 or more authors. Update all the endpoints to return the Book + Authors list. +- Add endpoints for assigning / removing an author from a Book +- Users of the library want to be able to checkout books for a period of time. Add a model to capture which books have been checked out. Include the checkout date and expected return date. Create a checkout api where you can try to checkout a book. You cannot check out books that are currently already borrowed. Add api routes for displaying books that are currently checked out, books that are overdue (should have been returned but are not returned). When you checkout a book, return the expected date for the return (2 weeks). To achieve this, you may want to look at query parameters for the filtering of the checked out books `?filter=someValue`. +- Swap out he InMemory db for another Postgres instance such as [neon](https://neon.tech/). Make sure you have ```appsettings.json``` AND ```appsettings.Development.json``` files in the root of the workshop.wwwapi project which contains suitable credentials. You may modify the seeding process if you need to. +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "ConnectionStrings": { + "DefaultConnectionString": "Host=HOST; Database=DATABASE; Username=USERNAME; Password=PASSWORD;" + + } +} + +``` + +Super Extensions (for the brave!) + +- Introduce the ability for Users to submit Book Reviews, which consist of a 5 star rating and comment on their view of the book. Users can submit these reviews anonymously or leave their email address. +- Implement both a generic IRepository AND Repository. Before you commit to this ensure that you understand the implications of the DbSet Include method on the generic repository! + + diff --git a/exercise.sln b/exercise.sln new file mode 100644 index 0000000..21e5ad0 --- /dev/null +++ b/exercise.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BF2A625B-9E2A-4434-834B-8858AB4F5AD7}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exercise.webapi", "exercise.webapi\exercise.webapi.csproj", "{E9C7E4C6-D843-4276-A6DE-C06FFE315453}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E9C7E4C6-D843-4276-A6DE-C06FFE315453}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9C7E4C6-D843-4276-A6DE-C06FFE315453}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9C7E4C6-D843-4276-A6DE-C06FFE315453}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9C7E4C6-D843-4276-A6DE-C06FFE315453}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9036C2A1-9F9D-4565-BE3F-411FE1341E8D} + EndGlobalSection +EndGlobal diff --git a/exercise.webapi/DTOs/AuthorDTO.cs b/exercise.webapi/DTOs/AuthorDTO.cs new file mode 100644 index 0000000..af1c38f --- /dev/null +++ b/exercise.webapi/DTOs/AuthorDTO.cs @@ -0,0 +1,31 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTOs +{ + public class AuthorDTO + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public List Books { get; set; } + + public AuthorDTO(Author author) + { + Id = author.Id; + FirstName = author.FirstName; + LastName = author.LastName; + Email = author.Email; + Books = author.Books?.Select(b => new BookListDTO(b)).ToList() ?? new List(); + } + + public AuthorDTO(int id, string firstName, string lastName, string email, List books = null) + { + Id = id; + FirstName = firstName; + LastName = lastName; + Email = email; + Books = books ?? new List(); + } + } +} diff --git a/exercise.webapi/DTOs/BookDTO.cs b/exercise.webapi/DTOs/BookDTO.cs new file mode 100644 index 0000000..216842f --- /dev/null +++ b/exercise.webapi/DTOs/BookDTO.cs @@ -0,0 +1,17 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTOs +{ + public class BookDTO + { + public int Id { get; set; } + public string Title { get; set; } + public AuthorDTO Author { get; set; } + public BookDTO(Book book) + { + Id = book.Id; + Title = book.Title; + Author = new AuthorDTO(book.Author); + } + } +} diff --git a/exercise.webapi/DTOs/BookListDTO.cs b/exercise.webapi/DTOs/BookListDTO.cs new file mode 100644 index 0000000..45f0536 --- /dev/null +++ b/exercise.webapi/DTOs/BookListDTO.cs @@ -0,0 +1,15 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.DTOs +{ + public class BookListDTO + { + public int Id { get; set; } + public string Title { get; set; } + public BookListDTO(Book book) + { + Id = book.Id; + Title = book.Title; + } + } +} diff --git a/exercise.webapi/DTOs/CreateBookDTO.cs b/exercise.webapi/DTOs/CreateBookDTO.cs new file mode 100644 index 0000000..8259133 --- /dev/null +++ b/exercise.webapi/DTOs/CreateBookDTO.cs @@ -0,0 +1,8 @@ +namespace exercise.webapi.DTOs +{ + public class CreateBookDTO + { + public string Title { get; set; } + public int AuthorId { get; set; } + } +} diff --git a/exercise.webapi/Data/DataContext.cs b/exercise.webapi/Data/DataContext.cs new file mode 100644 index 0000000..752be90 --- /dev/null +++ b/exercise.webapi/Data/DataContext.cs @@ -0,0 +1,39 @@ +using exercise.webapi.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace exercise.webapi.Data +{ + public class DataContext : DbContext + { + private readonly IConfiguration _configuration; + + public DataContext(DbContextOptions options) : base(options) + { + _configuration = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "..")) + .AddJsonFile("appsettings.json") + .Build(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + var connectionString = _configuration.GetConnectionString("DefaultConnection"); + optionsBuilder.UseNpgsql(connectionString); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + Seeder seeder = new Seeder(); + modelBuilder.Entity().HasData(seeder.Authors); + modelBuilder.Entity().HasData(seeder.Books); + } + + public DbSet Authors { get; set; } + public DbSet Books { get; set; } + } +} diff --git a/exercise.webapi/Data/Seeder.cs b/exercise.webapi/Data/Seeder.cs new file mode 100644 index 0000000..955e3c8 --- /dev/null +++ b/exercise.webapi/Data/Seeder.cs @@ -0,0 +1,117 @@ +using exercise.webapi.Models; + +namespace exercise.webapi.Data +{ + public class Seeder + { + private List _firstnames = new List() + { + "Audrey", + "Donald", + "Elvis", + "Barack", + "Oprah", + "Jimi", + "Mick", + "Kate", + "Charles", + "Kate" + }; + private List _lastnames = new List() + { + "Hepburn", + "Trump", + "Presley", + "Obama", + "Winfrey", + "Hendrix", + "Jagger", + "Winslet", + "Windsor", + "Middleton" + + }; + private List _domain = new List() + { + "bbc.co.uk", + "google.com", + "theworld.ca", + "something.com", + "tesla.com", + "nasa.org.us", + "gov.us", + "gov.gr", + "gov.nl", + "gov.ru" + }; + private List _firstword = new List() + { + "The", + "Two", + "Several", + "Fifteen", + "A bunch of", + "An army of", + "A herd of" + + + }; + private List _secondword = new List() + { + "Orange", + "Purple", + "Large", + "Microscopic", + "Green", + "Transparent", + "Rose Smelling", + "Bitter" + }; + private List _thirdword = new List() + { + "Buildings", + "Cars", + "Planets", + "Houses", + "Flowers", + "Leopards" + }; + + private List _authors = new List(); + private List _books = new List(); + + public Seeder() + { + + Random authorRandom = new Random(); + Random bookRandom = new Random(); + + + + for (int x = 1; x < 250; x++) + { + Author author = new Author(); + author.Id = x; + author.FirstName = _firstnames[authorRandom.Next(_firstnames.Count)]; + author.LastName = _lastnames[authorRandom.Next(_lastnames.Count)]; + author.Email = $"{author.FirstName}.{author.LastName}@{_domain[authorRandom.Next(_domain.Count)]}".ToLower(); + _authors.Add(author); + } + + + for (int y = 1; y < 250; y++) + { + Book book = new Book(); + book.Id = y; + book.Title = $"{_firstword[bookRandom.Next(_firstword.Count)]} {_secondword[bookRandom.Next(_secondword.Count)]} {_thirdword[bookRandom.Next(_thirdword.Count)]}"; + book.AuthorId = _authors[authorRandom.Next(_authors.Count)].Id; + //book.Author = authors[book.AuthorId-1]; + _books.Add(book); + } + + + } + public List Authors { get { return _authors; } } + public List Books { get { return _books; } } + } +} diff --git a/exercise.webapi/Endpoints/AuthorApi.cs b/exercise.webapi/Endpoints/AuthorApi.cs new file mode 100644 index 0000000..09804d9 --- /dev/null +++ b/exercise.webapi/Endpoints/AuthorApi.cs @@ -0,0 +1,33 @@ +using exercise.webapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.webapi.Endpoints +{ + public static class AuthorApi + { + public static void ConfigureAuthorApi(this WebApplication app) + { + app.MapGet("/authors", GetAuthors); + app.MapGet("/authors/{id}", GetAuthorById); + } + + private static async Task GetAuthors([FromServices] IAuthorRepository authorRepository) + { + var authors = await authorRepository.GetAllAuthors(); + return Results.Ok(authors); + } + + private static async Task GetAuthorById([FromServices] IAuthorRepository authorRepository, int id) + { + try + { + var author = await authorRepository.GetAuthorById(id); + return Results.Ok(author); + } + catch (KeyNotFoundException ex) + { + return Results.NotFound(new { message = ex.Message }); + } + } + } +} diff --git a/exercise.webapi/Endpoints/BookApi.cs b/exercise.webapi/Endpoints/BookApi.cs new file mode 100644 index 0000000..3a8cf14 --- /dev/null +++ b/exercise.webapi/Endpoints/BookApi.cs @@ -0,0 +1,65 @@ +using exercise.webapi.DTOs; +using exercise.webapi.Repository; + +namespace exercise.webapi.Endpoints +{ + public static class BookApi + { + public static void ConfigureBooksApi(this WebApplication app) + { + app.MapPost("/books", CreateBook); + app.MapGet("/books", GetBooks); + app.MapGet("/books/{id}", GetBookById); + app.MapPut("/books/{id}", UpdateBookAuthorById); + app.MapDelete("/books/{id}", DeleteBookById); + } + + private static async Task CreateBook(IBookRepository bookRepository, CreateBookDTO bookDto) + { + try + { + var createdBook = await bookRepository.CreateBook(bookDto); + return Results.Created($"/books/{createdBook.Id}", createdBook); + } + catch (KeyNotFoundException ex) + { + return Results.NotFound(new { message = ex.Message }); + } + catch (ArgumentException ex) + { + return Results.BadRequest(new { message = ex.Message }); + } + } + + private static async Task GetBooks(IBookRepository bookRepository) + { + var books = await bookRepository.GetAllBooks(); + return Results.Ok(books); + } + + private static async Task GetBookById(IBookRepository bookRepository, int id) + { + var book = await bookRepository.GetBookById(id); + return Results.Ok(book); + } + + private static async Task UpdateBookAuthorById(IBookRepository bookRepository, int id, int authorId) + { + var updatedBook = await bookRepository.UpdateBookAuthorById(id, authorId); + return Results.Ok(new { message = "Updated", book = updatedBook}); + } + + private static async Task DeleteBookById(IBookRepository bookRepository, int id) + { + try + { + await bookRepository.DeleteBookById(id); + return Results.Ok(new { message = $"Deleted book with id '{id}'"}); + } + catch (KeyNotFoundException ex) + { + return Results.BadRequest( new { message = $"No book with id '{id}' found"}); + } + } + } +} diff --git a/exercise.webapi/Models/Author.cs b/exercise.webapi/Models/Author.cs new file mode 100644 index 0000000..9124a49 --- /dev/null +++ b/exercise.webapi/Models/Author.cs @@ -0,0 +1,12 @@ + +namespace exercise.webapi.Models +{ + public class Author + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public ICollection Books { get; set; } = new List(); + } +} diff --git a/exercise.webapi/Models/Book.cs b/exercise.webapi/Models/Book.cs new file mode 100644 index 0000000..9298679 --- /dev/null +++ b/exercise.webapi/Models/Book.cs @@ -0,0 +1,11 @@ + +namespace exercise.webapi.Models +{ + public class Book + { + public int Id { get; set; } + public string Title { get; set; } + public int AuthorId { get; set; } + public Author Author { get; set; } + } +} diff --git a/exercise.webapi/Program.cs b/exercise.webapi/Program.cs new file mode 100644 index 0000000..042c07b --- /dev/null +++ b/exercise.webapi/Program.cs @@ -0,0 +1,36 @@ +using exercise.webapi.Data; +using exercise.webapi.Endpoints; +using exercise.webapi.Repository; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +var configuration = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "..")) + .AddJsonFile("appsettings.json") + .Build(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddDbContext(opt => + opt.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +using (var dbContext = new DataContext(new DbContextOptions())) +{ + dbContext.Database.EnsureCreated(); +} + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.ConfigureBooksApi(); +app.ConfigureAuthorApi(); +app.Run(); diff --git a/exercise.webapi/Properties/launchSettings.json b/exercise.webapi/Properties/launchSettings.json new file mode 100644 index 0000000..27b9975 --- /dev/null +++ b/exercise.webapi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:7277", + "sslPort": 44338 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7054;http://localhost:5201", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/exercise.webapi/Repository/AuthorRepository.cs b/exercise.webapi/Repository/AuthorRepository.cs new file mode 100644 index 0000000..20ebca0 --- /dev/null +++ b/exercise.webapi/Repository/AuthorRepository.cs @@ -0,0 +1,36 @@ +using exercise.webapi.Data; +using exercise.webapi.DTOs; +using Microsoft.EntityFrameworkCore; + +namespace exercise.webapi.Repository +{ + public class AuthorRepository : IAuthorRepository + { + DataContext _db; + + public AuthorRepository(DataContext db) + { + _db = db; + } + + public async Task> GetAllAuthors() + { + var authors = await _db.Authors.Include(a => a.Books).ToListAsync(); + + if (authors == null || !authors.Any()) + throw new KeyNotFoundException("No authors found."); + + return authors.Select(author => new AuthorDTO(author)); + } + + public async Task GetAuthorById(int id) + { + var author = await _db.Authors.Include(a => a.Books).FirstOrDefaultAsync(a => a.Id == id); + + if (author == null) + throw new KeyNotFoundException("Author not found."); + + return new AuthorDTO(author); + } + } +} diff --git a/exercise.webapi/Repository/BookRepository.cs b/exercise.webapi/Repository/BookRepository.cs new file mode 100644 index 0000000..0830b06 --- /dev/null +++ b/exercise.webapi/Repository/BookRepository.cs @@ -0,0 +1,86 @@ +using exercise.webapi.Data; +using exercise.webapi.DTOs; +using exercise.webapi.Models; +using Microsoft.EntityFrameworkCore; + +namespace exercise.webapi.Repository +{ + public class BookRepository: IBookRepository + { + DataContext _db; + + public BookRepository(DataContext db) + { + _db = db; + } + + public async Task CreateBook(CreateBookDTO bookDto) + { + var author = await _db.Authors.FirstOrDefaultAsync(a => a.Id == bookDto.AuthorId); + if (author == null) + throw new KeyNotFoundException("Author not found"); + + if (string.IsNullOrEmpty(bookDto.Title)) + throw new ArgumentException("Book title is required"); + + var book = new Book + { + Title = bookDto.Title, + AuthorId = bookDto.AuthorId, + Author = author + }; + + _db.Books.Add(book); + await _db.SaveChangesAsync(); + + return new BookDTO(book); + } + + + + public async Task> GetAllBooks() + { + var books = await _db.Books.Include(b => b.Author).ToListAsync(); + + if (books == null || !books.Any()) + throw new KeyNotFoundException("No books found."); + + return books.Select(book => new BookDTO(book)); + } + + public async Task GetBookById(int id) + { + var book = await _db.Books.Include(b => b.Author).FirstOrDefaultAsync(b => b.Id == id); + + if (book == null) + throw new KeyNotFoundException("Book not found"); + + return new BookDTO(book); + } + + public async Task UpdateBookAuthorById(int bookId, int authorId) + { + var book = await _db.Books.Include(b => b.Author).FirstOrDefaultAsync(b => b.Id == bookId) + ?? throw new KeyNotFoundException("Book not found"); + + var author = await _db.Authors.FirstOrDefaultAsync(a => a.Id == authorId) + ?? throw new KeyNotFoundException("Author not found"); + + book.Author = author; + book.AuthorId = author.Id; + + await _db.SaveChangesAsync(); + + return new BookDTO(book); + } + + public async Task DeleteBookById(int id) + { + var book = await _db.Books.FirstOrDefaultAsync(b => b.Id == id) + ?? throw new KeyNotFoundException("Book not found"); + + _db.Books.Remove(book); + await _db.SaveChangesAsync(); + } + } +} diff --git a/exercise.webapi/Repository/IAuthorRepository.cs b/exercise.webapi/Repository/IAuthorRepository.cs new file mode 100644 index 0000000..1fe8d31 --- /dev/null +++ b/exercise.webapi/Repository/IAuthorRepository.cs @@ -0,0 +1,10 @@ +using exercise.webapi.DTOs; + +namespace exercise.webapi.Repository +{ + public interface IAuthorRepository + { + public Task> GetAllAuthors(); + public Task GetAuthorById(int id); + } +} diff --git a/exercise.webapi/Repository/IBookRepository.cs b/exercise.webapi/Repository/IBookRepository.cs new file mode 100644 index 0000000..231ea79 --- /dev/null +++ b/exercise.webapi/Repository/IBookRepository.cs @@ -0,0 +1,17 @@ +using exercise.webapi.DTOs; + +namespace exercise.webapi.Repository +{ + public interface IBookRepository + { + public Task CreateBook(CreateBookDTO bookDto); + + public Task> GetAllBooks(); + + public Task GetBookById(int id); + + public Task UpdateBookAuthorById(int id, int authorId); + + public Task DeleteBookById(int id); + } +} diff --git a/exercise.webapi/exercise.webapi.csproj b/exercise.webapi/exercise.webapi.csproj new file mode 100644 index 0000000..a105476 --- /dev/null +++ b/exercise.webapi/exercise.webapi.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + enable + enable + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/exercise.webapi/exercise.webapi.http b/exercise.webapi/exercise.webapi.http new file mode 100644 index 0000000..2fcaed3 --- /dev/null +++ b/exercise.webapi/exercise.webapi.http @@ -0,0 +1,6 @@ +@exercise.webapi_HostAddress = http://localhost:5201 + +GET {{exercise.webapi_HostAddress}}/weatherforecast/ +Accept: application/json + +###