GraphQL API koji se koristi za deljenje location-based sadržaja izmedju korisnika. APi podržava dodavanje zemalja, gradova, mesta, korisnika i recenzija koje se vezuju za korisnike/mesta uz tačne koordinate.
/
Solution/
GraphQL.API/
GraphQL.Core/
GraphQL.Data/
GraphQL.Core
projekat definiše osnovne modele.
GraphQL.Data
projekat definiše sloj perzistencije.
Za perzistenciju se koristi mssql_server
baza podataka, dok je mapiranje ostvareno korišćenjem rešenja EntityFrameworkCore
. Mapiranje se ostvaruje kroz jedan kontekst. Pristup entitetima konteksta različitih tipova moguć je samo kroz repozitorijume. Postoji osnovni generički repozitorijum GenericRepository<T>
i po potrebi se proširuje dodatnim metodama.
Za komunikaciju sa izvorom podataka neophodno je navesti parametre u stringu koji opisuje konekciju koji će ostvariti vezu sa driver-om. Ukoliko se koristi docker
kontejner mcr.microsoft.com/mssql/server:2019-latest
sa podrazumevanim podešavanjima parametar konekcije je Server=localhost,1433;Database=medical;MultipleActiveResultSets=true;User=sa;Password=yourStrong(!)Password
.
public static IServiceCollection AddDbContext(this IServiceCollection services, IConfiguration configuration) =>
services
.AddDbContext<ApplicationContext>(options => options.UseSqlServer(
configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("GraphQL.API")))
.AddDatabaseDeveloperPageExceptionFilter();
GraphQL.API
projekat definiše API baziran na GraphQL schemi. Svi tipovi, mutacije, subscriptioni, upiti i sama schema se nalaze u ovom projektu. Kontroler poseduje samo jednu tačku koja parsuje upite i prosledjuje upravljanje executoru.
Kratak pregled tipa entiteta koji opisuju mesta. Asinhrona polja (poput location
i city
) se pribavljaju pozivom konkretnih repozitorijuma.
public class PlaceType : ObjectGraphType<Place>
{
public IServiceProvider Provider { get; set; }
public PlaceType(IServiceProvider provider)
{
Field(x => x.Id, type: typeof(IntGraphType));
Field(x => x.Name, type: typeof(StringGraphType));
Field<LocationType>("location", resolve: context => {
IGenericRepository<Location> locationRepository = (IGenericRepository<Location>)provider.GetService(typeof(IGenericRepository<Location>));
return locationRepository.GetById(context.Source.LocationId);
});
Field<CityType>("city", resolve: context => {
IGenericRepository<City> cityRepository = (IGenericRepository<City>)provider.GetService(typeof(IGenericRepository<City>));
return cityRepository.GetById(context.Source.CityId);
});
}
}
Upiti se koriste za pribavljanje entiteta odredjenog tipa. Tako, na primer, place
upit može da se koristi za pribavljanje mesta gde se preko repozitorjiuma pristupa kontekstu. Pribavljena mesta se mogu dodatno filtrirati name
argumentom za koji se očekuje poklapanje.
public class PlaceQuery : IFieldQueryServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<ListGraphType<PlaceType>>("places",
arguments: new QueryArguments(
new QueryArgument<StringGraphType> {Name = "name"}
),
resolve: context =>
{
var placeRepository = (IGenericRepository<Place>) sp.GetService(typeof(IGenericRepository<Place>));
var baseQuery = placeRepository.GetAll();
var name = context.GetArgument<string>("name");
return name != default(string) ? baseQuery.Where(w => w.Name.Contains(name)) : baseQuery.ToList();
});
}
}
Mutacije se zadaju kao akcije ulaznih argumenata upita, može se videti ulaz mutacije za dodavanje mesta addPlace
. Repozitorijumi se kao i u prethodnom slučaju probavljaju od Dependency Injection kontejnera i koriste za promenu konteksta.
public class AddPlaceMutation : IFieldMutationServiceItem
{
public void Activate(ObjectGraphType objectGraph, IWebHostEnvironment env, IServiceProvider sp)
{
objectGraph.Field<PlaceType>("addPlace",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<IntGraphType>> {Name = "cityId"},
new QueryArgument<NonNullGraphType<StringGraphType>> {Name = "placeName"},
new QueryArgument<FloatGraphType> {Name = "latitude"},
new QueryArgument<FloatGraphType> {Name = "longitude"}
),
resolve: context =>
{
var placeName = context.GetArgument<string>("placeName");
var cityId = context.GetArgument<int>("cityId");
var latitude = context.GetArgument<double>("latitude");
var longitude = context.GetArgument<double>("longitude");
var placeRepository = (IGenericRepository<Place>) sp.GetService(typeof(IGenericRepository<Place>));
var locationRepository = (IGenericRepository<Location>) sp.GetService(typeof(IGenericRepository<Location>));
...
Dodatno, postoji i jedan subscription koji se koristi prilikom dodavanja gradova. Slanjem poruka preko magistrale se obaveštavaju ostali korisnici API-a da je došlo do promene - u slučaju mutacije addCity
se tako šalje poruka CityAddedMessage
.
FieldService
servis vodi računa o registrovanju svih GraphQL tipova, mutacija i upita. Nije neophodno ručno registrovati nove upite, već se iz sadržaja assembly-a svi registruju, sudeći po njihovim osnovnim tipovima.
Za autentikaciju se koristi SSO provajdera Okta
i neophodno je konfigurisati domen u podešavanjima. Pritom, validacija autentikacije se oslanja na Bearer JSON web-tokene koji će biti neophodni u zaglavljima.
public static IServiceCollection AddAuthorization(this IServiceCollection services, IConfiguration configuration) =>
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultChallengeScheme = OktaDefaults.ApiAuthenticationScheme;
options.DefaultSignInScheme = OktaDefaults.ApiAuthenticationScheme;
})
.AddOktaWebApi(new OktaWebApiOptions()
{
OktaDomain = configuration
.GetSection(nameof(ApplicationOptions.Authentication))
.Get<AuthenticationOptions>().Domain
}).Services;
Neophodno je pribaviti token da bi se pristupilo APi-u. https://{okta-url}/oauth2/default/v1/token
je primer endpointa za dobijanje tokena. Očekuje se da se domen Okta
provajdera slaže sa domenom navedenim u Configuration["Authentication"]
.
Direktorijum /Extensions
sadrži extension metode za registraciju servisa u Dependency injection kontejner.
AddProjectServices
za registrovanje potrebnih servisaAddProjectRepositories
za registrovanje repozitorijuma koji se koriste za pristup entitetima kontekstaAddProjectSchema
za dodavanje svih potrebnih GraphQL tipovaAddDbContext
za povezivanje samssql_server
bazom podatakaAddCustomCaching
za dodavanje podrške keširanjaAddCustomCors
za konfigurisanje CORS pravilaAddCustomOptions
za registrovanje podešavanja izappsettings.json
datotekaAddCustomResponseCompression
za dodavanje servisa za kompresiju poputgzip
podrškeAddCustomRouting
za podešavanje rutiranja (lowercase rute)AddCustomHealthChecks
za dodavanje healthcheck servisa u kontejnerAddCustomGraphQL
za registrovanje svih potrebnih servisa GrphQL-a poput dodavanja tipova, omogućavanja socketaAddAuthorization
za dodavanje autorizacijeOkta
Single-Sign-On provajderomAddAuthorizationValidation
za validaciju autorizaije
Pipeline za obradu zahteva je proširen middleware delegatima.
UseServerTiming
za dodavanje zaglavlja o potrebnom vremenu obrade zahteva (samo u developmentu)UseDeveloperExceptionPage
za generisanje html stranica u slučaju grešaka (samo u developmentu)UseGraphQLPlayground
za development i testiranje APi-a,UseGraphQLVoyager
za produkcijuUseEndpoints
za dodavanje/status
endpointa u vidu healthcheckaUseAuthentication
iUseAuthorization
za autentikacijuUseResponseCompression
za kompresiju (primetno kodjson
datoteka npr)UseStaticFilesWithCacheControl
za serviranje statičkih datoteka sa podešavanjem izConfiguration["CacheProfiles"]
Sistem se pokreće nakon "izgradnje" pod-projekta GraphQL.API
.
Neophodno je pre svega izvršiti migracije nad bazom podataka:
$ dotnet ef migrations add InitialCreate
$ dotnet ef database update
Migracije se vrše iz pomenutog API pod-projekta jer je označen kao MigrationsAssembly
, iako su konteksti napisani u domenskom pod-projektu.
Na kraju, aplikativni sloj sadrži i graphql-playground
interfejs preko koga se mogu slati upiti.
query countries{
countries{
id,
name
}
}
query cities{
cities{
id,
name,
population,
country{
id,
name
}
}
}
query places{
places{
id,
name,
location {
latitude,
longitude
},
city {
id,
name,
population
}
}
}
query reviews{
reviews{
id,
description,
account {
id,
name,
email
},
place {
id,
name
}
}
}
query accounts{
accounts{
id,
name,
email,
dateOfBirth
}
}
mutation addCountry{
addCountry(countryName:"Serbia"){
id,
name
}
}
mutation addCityToSerbia{
addCity(countryId:1,cityName:"Belgrade",population:22200000){
id,
name,
population,
country{
id,
name
}
}
}
mutation addPlace{
addPlace(cityId: 1, placeName: "Some Tower in the center", latitude: 40.1231231234, longitude: 42.1231231233){
id,
name
}
}
mutation addReview{
addReview(description: "Something you have to say about it", placeId: 1, accountId: 1){
id,
description
}
}
mutation addAccount{
addAccount(name: "Name LastName", email: "[email protected]", dateOfBirth: "30/12/2021 05:50"){
id,
name
}
}
mutation addFriend{
addFriend(sourceId:1, destinationId: 2){
id,
name
}
}
mutation removeFriend{
removeFriend(sourceId:1, destinationId: 2){
id,
name
}
}
Otvoriti subscription u drugom pretraživaču i pratiti promene. Prilikom izdavanja novih query-a
za dodavanje gradova, promene će biti praćene.
subscription cityAddedToGermany{
cityAdded(countryName:"Germany"){
id,
cityName,
countryName,
message
}
}