You are on page 1of 19

Савремене софтверске архитектуре

– школска 2018/2019. година –

Пример 1. колоквијума

Задатак

Тип апликације: .NET Core Web Api 2 – трослојна архитектура

Напомена: Пројекат креирати на радној површини (desktop) у фолдеру „број индекса


– година уписа студија“ (нпр. 128-2015) и на крају га rar-овати (.rar),
након чега ће бити преузет од стране сарадника.

Креирати (локалну) базу података под називом ShopDB у оквиру које треба да
постоји табела Products са колонама:
Id (int, autoincrement, PK),
Name (nvarchar, NOT NULL),
Description (nvarchar, NULL),
Price (decimal, NOT NULL).
SQL код за базу података ставити заједно са Solution-ом у текстуални фајл.

Креирати одговарајући слој за повезивање на базу података (конекциони стринг


задати као атрибут repository класе) и одговарајући репозиторијум у оквиру тог слоја
(за креирану табелу), где треба да се налазе методе које врше унос података у базу,
као и преглед података из базе.
На слоју пословне логике треба направити следеће методе:
 једну која врши унос производа у базу (са провером да ли је унос успешан),
 једну која приказује све производе из базе, и
 једну која за задате две вредности у decimal формату приказује списак
производа који припадају овом рангу у односу на цену производа.

На Web Api слоју креирати одговарајући контролер који се може добити на


адреси: api/products заједно са акцијама на конкретним адресама:
 insert,
 getall,
 {minprice}/{maxprice}/get,
у складу са методама на слоју пословне логике.

Подаци треба да се приказују и уносе у JSON формату. Унос података проверити


помоћу Postman апликације.
Решење
1. Креирање базе
Ако није приказан, панел SQL Server Object Explorer укључујемо преко менија
View > SQL Server Object Explorer.
Потом у контекстном менију, који се добија десним кликом на Databases,
изаберемо опцију Add New Database, и у дијалогу базу назовемо ShopDB. За локацију
је пожељно оставити подразумеван фолдер како би се избегли проблем због
евентуалног ручног брисања или премештања фолдера са базом.
Затим у контекстном менију ставке са табелама (Tables) изаберемо опцију Add
New Table.

Добија се приказ као на следећој слици.


Променимо назив табеле у Producsts, а потом додајемо нова поља у табели као
што се тражи у задатку. За прво поље, Id, потребно је да буде и autoincrement, а то се
може постићи додавањем кључне речи IDENTITY у SQL коду (доњи панел на
претходној слици), или у Properties панелу (десни клик на Id врсту у горњем панелу
на претходној слици) се за Identity Specification изабере True за (Is Identity).
Након дефинисања колона у табели, добијамо приказ као на следећој слици.

Као што се примећује, за Description поље смо ставили ограничење на 100


карактера, а бира се таква вредност да ограничи опис производа на неки разуман
број карактера, ако подразумеваних 50 није довољно за већину производа.
За поље Price параметри у загради означавају максималан број целих и
децималних места. У зависности од врсте производа, број целих места може бити
већи или мањи, али у овом случају узимамо да је 8 цифара довољно (99 999 999.99).
Сада у горњем левом углу панела кликнемо на Update и у дијалогу након
генерисања скрипте кликнемо на тастер Update Database.
Сада ћемо унети пар производа како бисмо тестирали базу. У SQL Server Object
Explorer панелу десним тастером кликнемо на dbo.Products и одаберемо View Data.
Унесемо неколико производа, као што је приказано на следећој слици.
Сада у SQL Server Object Explorer панелу кликнемо десним тастером на
dbo.Products и изаберемо View Code, а потом додамо следећи код:
SELECT * FROM Products

Пре овога можемо обрисати постојећи код за креирање табеле, или испод тог
кода додати овај, а у другом случају код који желимо да се изврши морамо
селектовати, и након тога изаберемо на десни клик опцију као на слици, након чега
ћемо добити приказ свих производа из базе.

Такође, можемо извршити и упит са условом, као на следећој слици.


Овај корак није обавезан, али овако смо сигурни да се подаци из базе читају онако
како очекујемо.
На крају затворимо прозоре dbo.Table и dbo.Products, као и све друге који имају
везе са базом (осим SQL Server Object Explorer панела).

2. Креирање апликације
Одаберемо у менијима ставку File > New > Project, изаберемо .NET Core у левом
панелу и потом у десном ASP.NET Core Web Application, као на следећој слици.
За локацију одабрати фолдер који сте креирали на десктопу.
Након клика на OK тастер добија се дијалог као на следећој слици.

Одабрати API или Web API (зависи од инсталације VS2017), искључити подршку
за SSL (Configure for HTTPS), и потом кликнути на тастер OK. У Solution Explorer
панелу добијамо приказ као на следећој слици.

3. Креирање Data слоја


Из контекстног менија за Solution изаберемо Add > New Project, а потом Class
Library (.NET Core), као на следећој слици. За назив унесемо нпр. DataLayer.
Напомена: Често се може видети у литератури да се овај слој назива и DAL, што је
акроним од Data Access Layer.
Биће креирана и једна класа, чији назив променимо у ProductRepository (и у
Solution Explorer панелу и у коду – односно, треба променити и назив .cs фајла и назив
класе у самом коду).
Када се промени назив фајла, VS2017 ће сам понудити да се промени и назив
класе у коду, као што је приказано на следећој слици.

Сада у DataLayer пројекту креирамо фолдер Models (десни клик мишем > Add >
New Folder) и у том фолдеру креирамо једну класу (десни клик > Add > Class) и
назовемо је Product.
Сада у класи додамо атрибуте које представљају еквиваленте колона у табели
базе, при чему за саму класу ставимо да је јавна.
Сада треба додати код ProductRepository класи. Додајемо прво конекциони стринг:
private string ConnectionString =
"Server=(localdb)\\mssqllocaldb;Database=ShopDB;Trusted_Connection=True;
MultipleActiveResultSets=true";
Конекциони стринг се може прекопирати из неког од постојећих пројеката и
преправити (промени се назив базе и евентуално сервера ако има потребе).
Сада креирамо методу GetAllProducts(), с тим што ћемо прво креирати интерфејс
за ову класу, под називом IProductRepository. Из контекстног менија за DataLayer
одаберемо Add > New Item, и изаберемо из понуђених ставки Interface, који назовемо
IProductRepository. Интерфејс мора бити јаван, и садржи декларације свих метода
које имплементира ProductRepository класа, а које морају бити доступне вишим
слојевима апликације (јер се за DI користи интерфејс класе, а не сама класа).
Тако се добија код за IPRoductRepository.cs
using DataLayer.Models;
using System;
using System.Collections.Generic;
using System.Text;

namespace DataLayer
{
public interface IProductRepository
{
List<Product> GetAllProducts();
}
}

Код за методу GetAllProducts() урадимо аналогно као код примера са вежби, при
чему је за SQL потребно додати одговарајућу библиотеку, као на следећој слици, а
такође ProductRepository класа имплементира интерфејс IProductRepository.
Дакле, према ранијим примера, код за ProductRepository класу је:
public class ProductRepository : IProductRepository
{
private string ConnectionString =
"Server=(localdb)\\mssqllocaldb;Database=ShopDB;Trusted_Connection=Tru
e;MultipleActiveResultSets=true";

public List<Product> GetAllProducts()


{
List<Product> listToReturn = new List<Product>();

using(SqlConnection dataConnection = new


SqlConnection(this.ConnectionString))
{
dataConnection.Open();

SqlCommand command = new SqlCommand();


command.Connection = dataConnection;
command.CommandText = "SELECT * FROM Products";

SqlDataReader dataReader = command.ExecuteReader();

while (dataReader.Read())
{
Product product = new Product();
product.Id = dataReader.GetInt32(0);
product.Name = dataReader.GetString(1);
product.Description = dataReader.GetString(2);
product.Price = dataReader.GetDecimal(3);

listToReturn.Add(product);
}
}
return listToReturn;
}
}
Овде ће се касније појавити један проблем, а то је читање поља из базе која могу
бити празна (NULL поља) и за која при уносу података није задата никаква вредност.
То се може решити тако што за та поља уместо методе GetString() или друге за
одговарајући тип податка, користимо методу GetValue() и проверимо да ли је враћена
вредности null или одговарајућег типа (string и сл.).
На колоквијуму можете једноставно у та поља увек унети неку вредност.

Пошто је потребно креирати и методу за унос производа, па ћемо аналогно


ранијим примерима урадити и методу InsertProduct(), при чему ћемо се ограничити
на унос само једног производа (тј. нећемо имплементирати методу за унос листе
производа).
При упису у базу, одговарајућа метода враћа број уписаних врста, тако да је у
питању метода типа int, па у интерфејс класу додајемо следећи код:
int InsertProduct(Product product);
док у ProductRepository додајемо следећи код:
public int InsertProduct(Product product)
{
using(SqlConnection dataConnection = new
SqlConnection(this.ConnectionString))
{
dataConnection.Open();

SqlCommand command = new SqlCommand();


command.Connection = dataConnection;
command.CommandText = "INSERT INTO Products VALUES (" +
"'" + product.Name + "'" + ", " +
"'" + product.Description + "'" + ", " +
+ product.Price + ")";

return command.ExecuteNonQuery();
}
}

Касније ћемо Data слој допунити додатним кодом, ако буде потребе, а сада
прелазимо на креирање слоја пословне логике.
4. Креирање Business слоја
Истим поступком као за Data слој креирамо Business слој, који ћемо назвати
BusinessLayer, а аутоматски креирану класу преименујемо у ProductBusiness.
Први корак је да креирамо „везу“ ка Data слоју, користећи DI, као у следећем
коду:
using DataLayer;
using DataLayer.Models;
using System;
using System.Collections.Generic;

namespace BusinessLayer
{
public class ProductBusiness
{
private IProductRepository productRepository;

public ProductBusiness(IProductRepository productRepository)


{
this.productRepository = productRepository;
}

public List<Product> GetAllProducts()


{
List<Product> products =
this.productRepository.GetAllProducts();
if(products.Count > 0)
{
return products;
}
else
{
return null;
}
}
}
}
Остаје да креирамо интерфејс класу коју имплементира ProductBusiness, тј.
using DataLayer.Models;
using System;
using System.Collections.Generic;
using System.Text;

namespace BusinessLayer
{
public interface IProductBusiness
{
List<Product> GetAllProducts();
}
}
Остаје да још додамо код за методу која прослеђује Data слоју захтеве за упис у
базу, а која је слична одговарајућој методи у ранијим примерима који су рађени на
вежбама, тј. у интерфејс класу додајемо:
bool InsertProduct(Product product);
а у ProductBusiness класу:
public bool InsertProduct(Product product)
{
if(this.productRepository.InsertProduct(product) > 0)
{
return true;
}
else
{
return false;
}
}

InsertProduct је типа Boolean јер вишем слоју враћамо само податак о томе да ли
је упис у базу успешан или није, што је погодније за обраду на API слоју и frontend
апликацији.

На овом слоју ће бити потребно и креирати апликацију која враћа производе чија
је цена између задате минималне и максималне цене. То можемо урадити ефикасно
користећи FindAll() методу, за коју је документација дата на следећем линку:
https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-
1.findall?view=netcore-2.2
Као аргумент FindAll() методе навешћемо логичке услове да је цена производа
мања од максимално задате и већа од минимално задате цене, тј.
public List<Product> GetMinMaxPriceProducts(int minprice, int maxprice)
{
List<Product> products =
this.productRepository.GetAllProducts();

return products.FindAll(p => p.Price >= minprice && p.Price


<= maxprice);
}
а, наравно, потребно је додати декларацију ове методе и у интерфејс класу:
List<Product> GetMinMaxPriceProducts(int minprice, int maxprice);
5. Креирање контролера
Из контекстног менија за фолдер Controller на API слоју изаберемо ставку Add >
Controller… (ако нема ове ставке, онда изаберете New Item па у понуђеној листи
ставки изаберете API Controller или Web API Controller), при чему можете изабрати да
креирате празан контролер, као на следећој слици, или контролер са генерисаним
кодом за HTTP методе за читање и упис података.

Након потврде избора, у следећем дијалогу унесете назив контролера, као на


следећој слици. Контролер подразумевано има суфикс Controller.

Ако одаберете да креирате празан контролер, добија се следећи код (изостављен


је код са using декларацијама):
namespace ShopWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
}
}
У контролер класи прво креирамо „везу“ са Business слојем:
public class ProductController : ControllerBase
{
private IProductBusiness productBusiness;

public ProductController(IProductBusiness productBusiness)


{
this.productBusiness = productBusiness;
}
}

Затим додамо код за подразумевану GET методу, при чему треба имати у виду да
се у задатку и за приказ свих производа захтева посебна адреса, тј. на руту
контролера треба додати у путањи getall, што се може постићи на два начина,
додавањем Route[] декоратора методе, или аргументом саме HTTP методе, као што је
урађено у следећем коду:
[HttpGet("getall")]
public List<Product> GetAllProducts()
{
return this.productBusiness.GetAllProducts();
}

Такође, додајемо POST методу за унос производа, као у следећем коду:


[HttpPost("insert")]
public bool InsertProduct([FromBody] Product product)
{
return this.productBusiness.InsertProduct(product);
}

Остала је још метода за приказ производа чија је цена између две задате цене.
Задате цене се прослеђују као вредности у оквиру URL-а, а задају се у декоратору
HttpGet као у следећем коду:
[HttpGet("{minprice}/{maxprice}/get")]
public List<Product> GetMinMaxPriceProduct(int minprice, int
maxprice)
{
return
this.productBusiness.GetMinMaxPriceProducts(minprice, maxprice);
}
6. Конфигурација
Пре него што компајлирамо апликацију, потребно је у Startup.cs класи
инџектоване интерфејс класе декларисати као сервисе у оквиру ConfigureServices
методе, као у следећем коду:
public void ConfigureServices(IServiceCollection services)
{

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_
2_2);
services.AddScoped<IProductBusiness, ProductBusiness>();
services.AddScoped<IProductRepository,
ProductRepository>();
}
У случају да имате проблема са верзијом .NET Core SDK, тј. да је на рачунару нпр.
верзија 2.0 уместо верзије 2.2, морате уклонити прву декларацију у наведеној методи
којом се декларише ниво компатибилности са одговарајућом верзијом .NET Core.

7. Тестирање апликације
Након претходно описаног поступка компајлирамо/покренемо апликацију,
након чега се у веб прегледачу приказује подразумевани контролер. Позивањем
одговарајуће руте за приказ свих производа, добијамо приказ као на следећој слици.
За унос производа користимо Postman алат, а пошто се подаци о производу шаљу
у оквиру тела HTTP захтева, Postman треба подесити као на следећој слици, па се
након извршавања захтева добија позитиван одговор (true).

Након уноса неколико производа, можемо поново позвати методу за приказ свих
производа, након чега добијамо:
Ако сада позовемо руту за приказ производа из опсега задатих цена, добијамо
приказ као на следећој слици.

8. Закључак
Апликација успешно ради, што смо проверили „тестирањем“, али, као што смо
раније напоменули, може доћи до проблема ако не унесемо опис производа у базу.
Међутим, то се може догодити само ако производ уносимо директно у базу, ако унос
производа вршимо преко апликације, у случају изостављања описа у базу ће бити
уписан празан стринг, као на следећој слици:
тако да се поновним приказом свих производа може видети да нема проблема са
приказом производа који немају никакав опис.

За сваки случај, можемо у ProductRepository класи уместо кода


product.Description = dataReader.GetString(2);
ставити следећи код:
if (dataReader.GetValue(2) != null)
{
product.Description = dataReader.GetValue(2).ToString();
}
else
{
product.Description = "";
}
9. SQL код за табелу у бази

USE [ShopDB]
GO

/****** Object: Table [dbo].[Products] Script Date: 4/10/2019 8:02:27


PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Products] (


[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] NVARCHAR (50) NOT NULL,
[Description] NVARCHAR (100) NULL,
[Price] DECIMAL (8, 2) NOT NULL
);

You might also like