Πριν από μερικά χρόνια, πήρα το βιβλίο 'Pro ASP.NET Web API'. Αυτό το άρθρο είναι το απόσπασμα ιδεών από αυτό το βιβλίο, λίγο CQRS, και η δική μου εμπειρία στην ανάπτυξη συστημάτων διακομιστή-πελάτη.
Σε αυτό το άρθρο, θα καλύψω:
Το ASP.NET Core παρέχει πολλές βελτιώσεις σε σχέση με το ASP.NET MVC / Web API. Πρώτον, είναι τώρα ένα πλαίσιο και όχι δύο. Μου αρέσει πολύ γιατί είναι βολικό και υπάρχει λιγότερη σύγχυση. Δεύτερον, έχουμε κοντέινερ καταγραφής και DI χωρίς πρόσθετες βιβλιοθήκες, κάτι που με εξοικονομεί χρόνο και μου επιτρέπει να επικεντρωθώ στη σύνταξη καλύτερου κώδικα αντί να επιλέξω και να αναλύσουμε τις καλύτερες βιβλιοθήκες.
Ένας επεξεργαστής ερωτημάτων είναι μια προσέγγιση όταν όλη η επιχειρησιακή λογική που σχετίζεται με μία οντότητα του συστήματος είναι ενθυλακωμένη σε μία υπηρεσία και οποιαδήποτε πρόσβαση ή ενέργειες με αυτήν την οντότητα εκτελούνται μέσω αυτής της υπηρεσίας. Αυτή η υπηρεσία ονομάζεται συνήθως {EntityPluralName} QueryProcessor. Εάν είναι απαραίτητο, ένας επεξεργαστής ερωτήσεων περιλαμβάνει μεθόδους CRUD (δημιουργία, ανάγνωση, ενημέρωση, διαγραφή) για αυτήν την οντότητα. Ανάλογα με τις απαιτήσεις, δεν μπορούν να εφαρμοστούν όλες οι μέθοδοι. Για να δώσουμε ένα συγκεκριμένο παράδειγμα, ας ρίξουμε μια ματιά στο ChangePassword. Εάν η μέθοδος ενός επεξεργαστή ερωτημάτων απαιτεί δεδομένα εισόδου, τότε θα πρέπει να παρέχονται μόνο τα απαιτούμενα δεδομένα. Συνήθως, για κάθε μέθοδο, δημιουργείται μια ξεχωριστή κατηγορία ερωτημάτων, και σε απλές περιπτώσεις, είναι δυνατό (αλλά όχι επιθυμητό) να επαναχρησιμοποιηθεί η κλάση ερωτημάτων.
Σε αυτό το άρθρο, θα σας δείξω πώς να δημιουργήσετε ένα API για ένα μικρό σύστημα διαχείρισης κόστους, συμπεριλαμβανομένων βασικών ρυθμίσεων για έλεγχο ταυτότητας και ελέγχου πρόσβασης, αλλά δεν θα μπω στο υποσύστημα ελέγχου ταυτότητας. Θα καλύψω ολόκληρη τη λογική του συστήματος με αρθρωτές δοκιμές και θα δημιουργήσω τουλάχιστον μία δοκιμή ενοποίησης για κάθε μέθοδο API σε ένα παράδειγμα μιας οντότητας.
Απαιτήσεις για το ανεπτυγμένο σύστημα: Ο χρήστης μπορεί να προσθέσει, να επεξεργαστεί, να διαγράψει τα έξοδά του και να δει μόνο τα έξοδά του.
Ολόκληρος ο κωδικός αυτού του συστήματος είναι διαθέσιμος στις Github .
Ας αρχίσουμε λοιπόν να σχεδιάζουμε το μικρό αλλά πολύ χρήσιμο σύστημά μας.
Το διάγραμμα δείχνει ότι το σύστημα θα έχει τέσσερα επίπεδα:
Εκτός από τα περιγραφόμενα επίπεδα, έχουμε πολλές σημαντικές έννοιες. Το πρώτο είναι ο διαχωρισμός των μοντέλων δεδομένων. Το μοντέλο δεδομένων πελάτη χρησιμοποιείται κυρίως στο επίπεδο REST API. Μετατρέπει τα ερωτήματα σε μοντέλα τομέα και αντίστροφα από ένα μοντέλο τομέα σε ένα μοντέλο δεδομένων πελάτη, αλλά τα μοντέλα ερωτημάτων μπορούν επίσης να χρησιμοποιηθούν σε επεξεργαστές ερωτημάτων. Η μετατροπή γίνεται χρησιμοποιώντας το AutoMapper.
Χρησιμοποίησα το VS 2017 Professional για τη δημιουργία του έργου. Συνήθως μοιράζομαι τον πηγαίο κώδικα και τις δοκιμές σε διαφορετικούς φακέλους. Είναι άνετο, φαίνεται καλό, οι δοκιμές στο CI εκτελούνται βολικά και φαίνεται ότι η Microsoft συνιστά να το κάνετε με αυτόν τον τρόπο:
Περιγραφή Έργου:
Εργο | Περιγραφή |
---|---|
Εξοδα | Έργο για ελεγκτές, αντιστοίχιση μεταξύ μοντέλου τομέα και μοντέλου API, διαμόρφωση API |
Έξοδα.Api.Common | Σε αυτό το σημείο, συλλέγονται κλάσεις εξαίρεσης που ερμηνεύονται με συγκεκριμένο τρόπο από φίλτρα για την επιστροφή σωστών κωδικών HTTP με σφάλματα στον χρήστη |
Έξοδα.Api.Models | Έργο για μοντέλα API |
Έξοδα. Δεδομένα. Πρόσβαση | Σχέδιο διεπαφών και υλοποίηση του προτύπου Μονάδα Εργασίας |
Έξοδα. Data.Model | Έργο για μοντέλο τομέα |
Έξοδα. Ερωτήσεις | Έργο για επεξεργαστές ερωτημάτων και συγκεκριμένες κατηγορίες ερωτημάτων |
Έξοδα. Ασφάλεια | Έργο για τη διεπαφή και εφαρμογή του περιβάλλοντος ασφαλείας του τρέχοντος χρήστη |
Αναφορές μεταξύ έργων:
κοινά μεγέθη οθόνης για αποκριτικό σχεδιασμό
Έξοδα που δημιουργήθηκαν από το πρότυπο:
Άλλα έργα στο φάκελο src ανά πρότυπο:
Όλα τα έργα στο φάκελο δοκιμών ανά πρότυπο:
Αυτό το άρθρο δεν θα περιγράφει το μέρος που σχετίζεται με το περιβάλλον εργασίας χρήστη, αν και έχει εφαρμοστεί.
Το πρώτο βήμα ήταν να αναπτυχθεί ένα μοντέλο δεδομένων που βρίσκεται στη συναρμολόγηση Expenses.Data.Model
:
Το Expense
Η κλάση περιέχει τα ακόλουθα χαρακτηριστικά:
public class Expense { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public virtual User User { get; set; } public bool IsDeleted { get; set; } }
Αυτό το μάθημα υποστηρίζει «μαλακή διαγραφή» μέσω του IsDeleted
χαρακτηριστικό και περιέχει όλα τα δεδομένα για μία δαπάνη ενός συγκεκριμένου χρήστη που θα μας είναι χρήσιμα στο μέλλον.
Τα User
, Role
, και UserRole
τάξεις αναφέρονται στο υποσύστημα πρόσβασης. Αυτό το σύστημα δεν προσποιείται ότι είναι το σύστημα του έτους και η περιγραφή αυτού του υποσυστήματος δεν είναι ο σκοπός αυτού του άρθρου. Επομένως, το μοντέλο δεδομένων και ορισμένες λεπτομέρειες της εφαρμογής θα παραλειφθούν. Το σύστημα οργάνωσης πρόσβασης μπορεί να αντικατασταθεί από ένα πιο τέλειο, χωρίς να αλλάξει η επιχειρηματική λογική.
Στη συνέχεια, το πρότυπο Unit of Work εφαρμόστηκε στο Expenses.Data.Access
συναρμολόγηση, φαίνεται η δομή αυτού του έργου:
Απαιτούνται οι ακόλουθες βιβλιοθήκες για συναρμολόγηση:
Microsoft.EntityFrameworkCore.SqlServer
Είναι απαραίτητο να εφαρμοστεί ένα EF
περιβάλλον που θα εντοπίσει αυτόματα τις αντιστοιχίσεις σε έναν συγκεκριμένο φάκελο:
public class MainDbContext : DbContext { public MainDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { var mappings = MappingsHelper.GetMainMappings(); foreach (var mapping in mappings) { mapping.Visit(modelBuilder); } } }
Η χαρτογράφηση γίνεται μέσω του MappingsHelper
τάξη:
public static class MappingsHelper { public static IEnumerable GetMainMappings() { var assemblyTypes = typeof(UserMap).GetTypeInfo().Assembly.DefinedTypes; var mappings = assemblyTypes // ReSharper disable once AssignNullToNotNullAttribute .Where(t => t.Namespace != null && t.Namespace.Contains(typeof(UserMap).Namespace)) .Where(t => typeof(IMap).GetTypeInfo().IsAssignableFrom(t)); mappings = mappings.Where(x => !x.IsAbstract); return mappings.Select(m => (IMap) Activator.CreateInstance(m.AsType())).ToArray(); } }
Η αντιστοίχιση στα μαθήματα βρίσκεται στο Maps
φάκελος και αντιστοίχιση για Expenses
:
public class ExpenseMap : IMap { public void Visit(ModelBuilder builder) { builder.Entity() .ToTable('Expenses') .HasKey(x => x.Id); } }
Διεπαφή IUnitOfWork
:
public interface IUnitOfWork : IDisposable { ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot); void Add(T obj) where T: class ; void Update(T obj) where T : class; void Remove(T obj) where T : class; IQueryable Query() where T : class; void Commit(); Task CommitAsync(); void Attach(T obj) where T : class; }
Η εφαρμογή του είναι ένα περιτύλιγμα για EF DbContext
:
τι μπορεί να κάνει το ρουμπίνι στις ράγες
public class EFUnitOfWork : IUnitOfWork { private DbContext _context; public EFUnitOfWork(DbContext context) { _context = context; } public DbContext Context => _context; public ITransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Snapshot) { return new DbTransaction(_context.Database.BeginTransaction(isolationLevel)); } public void Add(T obj) where T : class { var set = _context.Set(); set.Add(obj); } public void Update(T obj) where T : class { var set = _context.Set(); set.Attach(obj); _context.Entry(obj).State = EntityState.Modified; } void IUnitOfWork.Remove(T obj) { var set = _context.Set(); set.Remove(obj); } public IQueryable Query() where T : class { return _context.Set(); } public void Commit() { _context.SaveChanges(); } public async Task CommitAsync() { await _context.SaveChangesAsync(); } public void Attach(T newUser) where T : class { var set = _context.Set(); set.Attach(newUser); } public void Dispose() { _context = null; } }
Η διεπαφή ITransaction
δεν θα χρησιμοποιηθεί:
public interface ITransaction : IDisposable { void Commit(); void Rollback(); }
Η εφαρμογή του απλώς τυλίγει το EF
συναλλαγή:
public class DbTransaction : ITransaction { private readonly IDbContextTransaction _efTransaction; public DbTransaction(IDbContextTransaction efTransaction) { _efTransaction = efTransaction; } public void Commit() { _efTransaction.Commit(); } public void Rollback() { _efTransaction.Rollback(); } public void Dispose() { _efTransaction.Dispose(); } }
Επίσης σε αυτό το στάδιο, για τις δοκιμές μονάδας, το ISecurityContext
απαιτείται διεπαφή, η οποία καθορίζει τον τρέχοντα χρήστη του API (το έργο είναι Expenses.Security
):
public interface ISecurityContext { User User { get; } bool IsAdministrator { get; } }
Στη συνέχεια, πρέπει να ορίσετε τη διεπαφή και την εφαρμογή του επεξεργαστή ερωτημάτων, ο οποίος θα περιέχει όλη τη λογική της επιχείρησης για εργασία με κόστος - στην περίπτωσή μας, IExpensesQueryProcessor
και ExpensesQueryProcessor
:
public interface IExpensesQueryProcessor { IQueryable Get(); Expense Get(int id); Task Create(CreateExpenseModel model); Task Update(int id, UpdateExpenseModel model); Task Delete(int id); } public class ExpensesQueryProcessor : IExpensesQueryProcessor { public IQueryable Get() { throw new NotImplementedException(); } public Expense Get(int id) { throw new NotImplementedException(); } public Task Create(CreateExpenseModel model) { throw new NotImplementedException(); } public Task Update(int id, UpdateExpenseModel model) { throw new NotImplementedException(); } public Task Delete(int id) { throw new NotImplementedException(); } }
Το επόμενο βήμα είναι να διαμορφώσετε το Expenses.Queries.Tests
συνέλευση. Εγκατέστησα τις ακόλουθες βιβλιοθήκες:
Στη συνέχεια, στο Expenses.Queries.Tests
συναρμολόγηση, καθορίζουμε το προσάρτημα για δοκιμές μονάδας και περιγράφουμε τις δοκιμές μονάδας μας:
public class ExpensesQueryProcessorTests { private Mock _uow; private List _expenseList; private IExpensesQueryProcessor _query; private Random _random; private User _currentUser; private Mock _securityContext; public ExpensesQueryProcessorTests() { _random = new Random(); _uow = new Mock(); _expenseList = new List(); _uow.Setup(x => x.Query()).Returns(() => _expenseList.AsQueryable()); _currentUser = new User{Id = _random.Next()}; _securityContext = new Mock(MockBehavior.Strict); _securityContext.Setup(x => x.User).Returns(_currentUser); _securityContext.Setup(x => x.IsAdministrator).Returns(false); _query = new ExpensesQueryProcessor(_uow.Object, _securityContext.Object); } [Fact] public void GetShouldReturnAll() { _expenseList.Add(new Expense{UserId = _currentUser.Id}); var result = _query.Get().ToList(); result.Count.Should().Be(1); } [Fact] public void GetShouldReturnOnlyUserExpenses() { _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get().ToList(); result.Count().Should().Be(1); result[0].UserId.Should().Be(_currentUser.Id); } [Fact] public void GetShouldReturnAllExpensesForAdministrator() { _securityContext.Setup(x => x.IsAdministrator).Returns(true); _expenseList.Add(new Expense { UserId = _random.Next() }); _expenseList.Add(new Expense { UserId = _currentUser.Id }); var result = _query.Get(); result.Count().Should().Be(2); } [Fact] public void GetShouldReturnAllExceptDeleted() { _expenseList.Add(new Expense { UserId = _currentUser.Id }); _expenseList.Add(new Expense { UserId = _currentUser.Id, IsDeleted = true}); var result = _query.Get(); result.Count().Should().Be(1); } [Fact] public void GetShouldReturnById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); var result = _query.Get(expense.Id); result.Should().Be(expense); } [Fact] public void GetShouldThrowExceptionIfExpenseOfOtherUser() { var expense = new Expense { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow(); } [Fact] public void GetShouldThrowExceptionIfItemIsNotFoundById() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id }; _expenseList.Add(expense); Action get = () => { _query.Get(_random.Next()); }; get.ShouldThrow(); } [Fact] public void GetShouldThrowExceptionIfUserIsDeleted() { var expense = new Expense { Id = _random.Next(), UserId = _currentUser.Id, IsDeleted = true}; _expenseList.Add(expense); Action get = () => { _query.Get(expense.Id); }; get.ShouldThrow(); } [Fact] public async Task CreateShouldSaveNew() { var model = new CreateExpenseModel { Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now }; var result = await _query.Create(model); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); result.UserId.Should().Be(_currentUser.Id); _uow.Verify(x => x.Add(result)); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task UpdateShouldUpdateFields() { var user = new Expense {Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); var model = new UpdateExpenseModel { Comment = _random.Next().ToString(), Description = _random.Next().ToString(), Amount = _random.Next(), Date = DateTime.Now }; var result = await _query.Update(user.Id, model); result.Should().Be(user); result.Description.Should().Be(model.Description); result.Amount.Should().Be(model.Amount); result.Comment.Should().Be(model.Comment); result.Date.Should().BeCloseTo(model.Date); _uow.Verify(x => x.CommitAsync()); } [Fact] public void UpdateShoudlThrowExceptionIfItemIsNotFound() { Action create = () => { var result = _query.Update(_random.Next(), new UpdateExpenseModel()).Result; }; create.ShouldThrow(); } [Fact] public async Task DeleteShouldMarkAsDeleted() { var user = new Expense() { Id = _random.Next(), UserId = _currentUser.Id}; _expenseList.Add(user); await _query.Delete(user.Id); user.IsDeleted.Should().BeTrue(); _uow.Verify(x => x.CommitAsync()); } [Fact] public async Task DeleteShoudlThrowExceptionIfItemIsNotBelongTheUser() { var expense = new Expense() { Id = _random.Next(), UserId = _random.Next() }; _expenseList.Add(expense); Action execute = () => { _query.Delete(expense.Id).Wait(); }; execute.ShouldThrow(); } [Fact] public void DeleteShoudlThrowExceptionIfItemIsNotFound() { Action execute = () => { _query.Delete(_random.Next()).Wait(); }; execute.ShouldThrow(); }
Μετά την περιγραφή των δοκιμών μονάδας, περιγράφεται η εφαρμογή ενός επεξεργαστή ερωτημάτων:
public class ExpensesQueryProcessor : IExpensesQueryProcessor { private readonly IUnitOfWork _uow; private readonly ISecurityContext _securityContext; public ExpensesQueryProcessor(IUnitOfWork uow, ISecurityContext securityContext) { _uow = uow; _securityContext = securityContext; } public IQueryable Get() { var query = GetQuery(); return query; } private IQueryable GetQuery() { var q = _uow.Query() .Where(x => !x.IsDeleted); if (!_securityContext.IsAdministrator) { var userId = _securityContext.User.Id; q = q.Where(x => x.UserId == userId); } return q; } public Expense Get(int id) { var user = GetQuery().FirstOrDefault(x => x.Id == id); if (user == null) { throw new NotFoundException('Expense is not found'); } return user; } public async Task Create(CreateExpenseModel model) { var item = new Expense { UserId = _securityContext.User.Id, Amount = model.Amount, Comment = model.Comment, Date = model.Date, Description = model.Description, }; _uow.Add(item); await _uow.CommitAsync(); return item; } public async Task Update(int id, UpdateExpenseModel model) { var expense = GetQuery().FirstOrDefault(x => x.Id == id); if (expense == null) { throw new NotFoundException('Expense is not found'); } expense.Amount = model.Amount; expense.Comment = model.Comment; expense.Description = model.Description; expense.Date = model.Date; await _uow.CommitAsync(); return expense; } public async Task Delete(int id) { var user = GetQuery().FirstOrDefault(u => u.Id == id); if (user == null) { throw new NotFoundException('Expense is not found'); } if (user.IsDeleted) return; user.IsDeleted = true; await _uow.CommitAsync(); } }
Μόλις η επιχειρησιακή λογική είναι έτοιμη, αρχίζω να γράφω τις δοκιμές ενοποίησης API για να προσδιορίσω τη σύμβαση API.
Το πρώτο βήμα είναι η προετοιμασία ενός έργου Expenses.Api.IntegrationTests
[CollectionDefinition('ApiCollection')] public class DbCollection : ICollectionFixture { } ~~~ And define our test server and the client to it with the already authenticated user by default:
δημόσια κλάση ApiServer: IDisposable {public const string Username = 'admin'; public const string Κωδικός πρόσβασης = 'admin';
private IConfigurationRoot _config; public ApiServer() { _config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile('appsettings.json') .Build(); Server = new TestServer(new WebHostBuilder().UseStartup()); Client = GetAuthenticatedClient(Username, Password); } public HttpClient GetAuthenticatedClient(string username, string password) { var client = Server.CreateClient(); var response = client.PostAsync('/api/Login/Authenticate', new JsonContent(new LoginModel {Password = password, Username = username})).Result; response.EnsureSuccessStatusCode(); var data = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result); client.DefaultRequestHeaders.Add('Authorization', 'Bearer ' + data.Token); return client; } public HttpClient Client { get; private set; } public TestServer Server { get; private set; } public void Dispose() { if (Client != null) { Client.Dispose(); Client = null; } if (Server != null) { Server.Dispose(); Server = null; } } } ~~~
Για την ευκολία εργασίας με HTTP
αιτήματα σε δοκιμές ενσωμάτωσης, έγραψα έναν βοηθό:
public class HttpClientWrapper { private readonly HttpClient _client; public HttpClientWrapper(HttpClient client) { _client = client; } public HttpClient Client => _client; public async Task PostAsync(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject(respnoseText); return data; } public async Task PostAsync(string url, object body) { var response = await _client.PostAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); } public async Task PutAsync(string url, object body) { var response = await _client.PutAsync(url, new JsonContent(body)); response.EnsureSuccessStatusCode(); var respnoseText = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject(respnoseText); return data; } }
Σε αυτό το στάδιο, πρέπει να ορίσω ένα συμβόλαιο REST API για κάθε οντότητα, θα το γράψω για έξοδα API REST:
Διεύθυνση URL | Μέθοδος | Σωματότυπος | Τύπος αποτελεσμάτων | Περιγραφή |
---|---|---|---|---|
Δαπάνη | ΠΑΙΡΝΩ | - | Αποτέλεσμα δεδομένων | Λάβετε όλα τα έξοδα με πιθανή χρήση φίλτρων και ταξινομητών σε μια παράμετρο ερωτήματος «εντολές» |
Έξοδα / {id} | ΠΑΙΡΝΩ | - | Μοντέλο εξόδων | Λάβετε έξοδα ανά αναγνωριστικό |
Εξοδα | ΘΕΣΗ | Δημιουργία ΜοντέλουExpense | Μοντέλο εξόδων | Δημιουργήστε νέα εγγραφή εξόδων |
Έξοδα / {id} | ΒΑΖΩ | ΕνημέρωσηExpenseModel | Μοντέλο εξόδων | Ενημέρωση υπάρχοντος κόστους |
Όταν ζητάτε μια λίστα με τα κόστη, μπορείτε να εφαρμόσετε διάφορες εντολές φιλτραρίσματος και ταξινόμησης χρησιμοποιώντας το Βιβλιοθήκη AutoQueryable . Ένα παράδειγμα ερωτήματος με φιλτράρισμα και ταξινόμηση:
/expenses?commands=take=25%26amount%3E=12%26orderbydesc=date
Η τιμή της παραμέτρου εντολών αποκωδικοποίησης είναι take=25&amount>=12&orderbydesc=date
. Έτσι μπορούμε να βρούμε σελιδοποίηση, φιλτράρισμα και ταξινόμηση τμημάτων στο ερώτημα. Όλες οι επιλογές ερωτήματος είναι πολύ παρόμοιες με τη σύνταξη OData, αλλά δυστυχώς, το OData δεν είναι ακόμη έτοιμο για το .NET Core, οπότε χρησιμοποιώ μια άλλη χρήσιμη βιβλιοθήκη.
λάβετε στοιχείο ανά κατηγορία angularjs
Στο κάτω μέρος εμφανίζονται όλα τα μοντέλα που χρησιμοποιούνται σε αυτό το API:
public class DataResult { public T[] Data { get; set; } public int Total { get; set; } } public class ExpenseModel { public int Id { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public decimal Amount { get; set; } public string Comment { get; set; } public int UserId { get; set; } public string Username { get; set; } } public class CreateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } } public class UpdateExpenseModel { [Required] public DateTime Date { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, int.MaxValue)] public decimal Amount { get; set; } [Required] public string Comment { get; set; } }
Μοντέλα CreateExpenseModel
και UpdateExpenseModel
Χρησιμοποιήστε χαρακτηριστικά σχολιασμού δεδομένων για να πραγματοποιήσετε απλούς ελέγχους σε επίπεδο REST API μέσω χαρακτηριστικών.
Στη συνέχεια, για κάθε HTTP
μέθοδος, ένας ξεχωριστός φάκελος δημιουργείται στο έργο και τα αρχεία σε αυτό δημιουργούνται με βάση για κάθε HTTP
μέθοδο που υποστηρίζεται από τον πόρο:
Υλοποίηση του τεστ ενοποίησης για τη λήψη καταλόγου δαπανών:
[Collection('ApiCollection')] public class GetListShould { private readonly ApiServer _server; private readonly HttpClient _client; public GetListShould(ApiServer server) { _server = server; _client = server.Client; } public static async Task Get(HttpClient client) { var response = await client.GetAsync($'api/Expenses'); response.EnsureSuccessStatusCode(); var responseText = await response.Content.ReadAsStringAsync(); var items = JsonConvert.DeserializeObject(responseText); return items; } [Fact] public async Task ReturnAnyList() { var items = await Get(_client); items.Should().NotBeNull(); } }
Υλοποίηση του τεστ ενοποίησης για τη λήψη των δεδομένων δαπανών ανά αναγνωριστικό
[Collection('ApiCollection')] public class GetItemShould { private readonly ApiServer _server; private readonly HttpClient _client; private Random _random; public GetItemShould(ApiServer server) { _server = server; _client = _server.Client; _random = new Random(); } [Fact] public async Task ReturnItemById() { var item = await new PostShould(_server).CreateNew(); var result = await GetById(_client, item.Id); result.Should().NotBeNull(); } public static async Task GetById(HttpClient client, int id) { var response = await client.GetAsync(new Uri($'api/Expenses/{id}', UriKind.Relative)); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(result); } [Fact] public async Task ShouldReturn404StatusIfNotFound() { var response = await _client.GetAsync(new Uri($'api/Expenses/-1', UriKind.Relative)); response.StatusCode.ShouldBeEquivalentTo(HttpStatusCode.NotFound); } }
Υλοποίηση του τεστ ενοποίησης για τη δημιουργία εξόδων:
[Collection('ApiCollection')] public class PostShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private Random _random; public PostShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task CreateNew() { var requestItem = new CreateExpenseModel() { Amount = _random.Next(), Comment = _random.Next().ToString(), Date = DateTime.Now.AddMinutes(-15), Description = _random.Next().ToString() }; var createdItem = await _client.PostAsync('api/Expenses', requestItem); createdItem.Id.Should().BeGreaterThan(0); createdItem.Amount.Should().Be(requestItem.Amount); createdItem.Comment.Should().Be(requestItem.Comment); createdItem.Date.Should().Be(requestItem.Date); createdItem.Description.Should().Be(requestItem.Description); createdItem.Username.Should().Be('admin admin'); return createdItem; } }
Εφαρμογή του τεστ ενοποίησης για αλλαγή δαπανών:
[Collection('ApiCollection')] public class PutShould { private readonly ApiServer _server; private readonly HttpClientWrapper _client; private readonly Random _random; public PutShould(ApiServer server) { _server = server; _client = new HttpClientWrapper(_server.Client); _random = new Random(); } [Fact] public async Task UpdateExistingItem() { var item = await new PostShould(_server).CreateNew(); var requestItem = new UpdateExpenseModel { Date = DateTime.Now, Description = _random.Next().ToString(), Amount = _random.Next(), Comment = _random.Next().ToString() }; await _client.PutAsync($'api/Expenses/{item.Id}', requestItem); var updatedItem = await GetItemShould.GetById(_client.Client, item.Id); updatedItem.Date.Should().Be(requestItem.Date); updatedItem.Description.Should().Be(requestItem.Description); updatedItem.Amount.Should().Be(requestItem.Amount); updatedItem.Comment.Should().Contain(requestItem.Comment); } }
Υλοποίηση του τεστ ενοποίησης για την αφαίρεση εξόδων:
[Collection('ApiCollection')] public class DeleteShould { private readonly ApiServer _server; private readonly HttpClient _client; public DeleteShould(ApiServer server) { _server = server; _client = server.Client; } [Fact] public async Task DeleteExistingItem() { var item = await new PostShould(_server).CreateNew(); var response = await _client.DeleteAsync(new Uri($'api/Expenses/{item.Id}', UriKind.Relative)); response.EnsureSuccessStatusCode(); } }
Σε αυτό το σημείο, έχουμε ορίσει πλήρως το συμβόλαιο REST API και τώρα μπορώ να αρχίσω να το εφαρμόζω βάσει του ASP.NET Core.
Προετοιμάστε το Έργο Έξοδα. Για αυτό, πρέπει να εγκαταστήσω τις ακόλουθες βιβλιοθήκες:
Μετά από αυτό, πρέπει να ξεκινήσετε τη δημιουργία της αρχικής μετεγκατάστασης για τη βάση δεδομένων ανοίγοντας το Package Manager Console, μεταβαίνοντας στο Expenses.Data.Access
έργο (επειδή το πλαίσιο EF
βρίσκεται εκεί) και εκτελεί το Add-Migration InitialCreate
εντολή:
Στο επόμενο βήμα, προετοιμάστε το αρχείο διαμόρφωσης appsettings.json εκ των προτέρων, το οποίο μετά την προετοιμασία θα πρέπει ακόμη να αντιγραφεί στο έργο Expenses.Api.IntegrationTests
γιατί από εκεί, θα τρέξουμε το δοκιμαστικό API παρουσίας.
{ 'Logging': { 'IncludeScopes': false, 'LogLevel': { 'Default': 'Debug', 'System': 'Information', 'Microsoft': 'Information' } }, 'Data': { 'main': 'Data Source=.; Initial Catalog=expenses.main; Integrated Security=true; Max Pool Size=1000; Min Pool Size=12; Pooling=True;' }, 'ApplicationInsights': { 'InstrumentationKey': 'Your ApplicationInsights key' } }
Η ενότητα καταγραφής δημιουργείται αυτόματα. Πρόσθεσα το Data
ενότητα για να αποθηκεύσετε τη συμβολοσειρά σύνδεσης στη βάση δεδομένων και το ApplicationInsights
κλειδί.
Πρέπει να διαμορφώσετε διαφορετικές υπηρεσίες διαθέσιμες στην εφαρμογή μας:
Ενεργοποίηση του ApplicationInsights
: services.AddApplicationInsightsTelemetry(Configuration);
Καταχωρήστε τις υπηρεσίες σας μέσω κλήσης: ContainerSetup.Setup(services, Configuration);
ContainerSetup
είναι μια τάξη που δημιουργήθηκε, οπότε δεν χρειάζεται να αποθηκεύουμε όλες τις εγγραφές υπηρεσιών στο Startup
τάξη. Η τάξη βρίσκεται στο φάκελο IoC του έργου Έξοδα:
public static class ContainerSetup { public static void Setup(IServiceCollection services, IConfigurationRoot configuration) { AddUow(services, configuration); AddQueries(services); ConfigureAutoMapper(services); ConfigureAuth(services); } private static void ConfigureAuth(IServiceCollection services) { services.AddSingleton(); services.AddScoped(); services.AddScoped(); } private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient(); } private static void AddUow(IServiceCollection services, IConfigurationRoot configuration) { var connectionString = configuration['Data:main']; services.AddEntityFrameworkSqlServer(); services.AddDbContext(options => options.UseSqlServer(connectionString)); services.AddScoped(ctx => new EFUnitOfWork(ctx.GetRequiredService())); services.AddScoped(); services.AddScoped(); } private static void AddQueries(IServiceCollection services) { var exampleProcessorType = typeof(UsersQueryProcessor); var types = (from t in exampleProcessorType.GetTypeInfo().Assembly.GetTypes() where t.Namespace == exampleProcessorType.Namespace && t.GetTypeInfo().IsClass && t.GetTypeInfo().GetCustomAttribute() == null select t).ToArray(); foreach (var type in types) { var interfaceQ = type.GetTypeInfo().GetInterfaces().First(); services.AddScoped(interfaceQ, type); } } }
Σχεδόν όλοι οι κώδικες σε αυτήν την τάξη μιλούν από μόνες τους, αλλά θα ήθελα να μπω στο ConfigureAutoMapper
μέθοδος λίγο περισσότερο.
private static void ConfigureAutoMapper(IServiceCollection services) { var mapperConfig = AutoMapperConfigurator.Configure(); var mapper = mapperConfig.CreateMapper(); services.AddSingleton(x => mapper); services.AddTransient(); }
Αυτή η μέθοδος χρησιμοποιεί την τάξη βοηθού για να βρει όλες τις αντιστοιχίσεις μεταξύ μοντέλων και οντοτήτων και το αντίστροφο και παίρνει το IMapper
διεπαφή για να δημιουργήσετε το IAutoMapper
περιτύλιγμα που θα χρησιμοποιηθεί σε ελεγκτές. Δεν υπάρχει τίποτα το ιδιαίτερο για αυτό το περιτύλιγμα - παρέχει απλώς μια βολική διεπαφή στο AutoMapper
μεθόδους.
public class AutoMapperAdapter : IAutoMapper { private readonly IMapper _mapper; public AutoMapperAdapter(IMapper mapper) { _mapper = mapper; } public IConfigurationProvider Configuration => _mapper.ConfigurationProvider; public T Map(object objectToMap) { return _mapper.Map(objectToMap); } public TResult[] Map(IEnumerable sourceQuery) { return sourceQuery.Select(x => _mapper.Map(x)).ToArray(); } public IQueryable Map(IQueryable sourceQuery) { return sourceQuery.ProjectTo(_mapper.ConfigurationProvider); } public void Map(TSource source, TDestination destination) { _mapper.Map(source, destination); } }
Για να ρυθμίσετε το AutoMapper, χρησιμοποιείται η βοηθητική κλάση, η αποστολή της οποίας είναι η αναζήτηση αντιστοιχίσεων για συγκεκριμένες κλάσεις χώρου ονομάτων. Όλες οι αντιστοιχίσεις βρίσκονται στο φάκελο Έξοδα / Χάρτες:
llc vs s corp vs c corp
public static class AutoMapperConfigurator { private static readonly object Lock = new object(); private static MapperConfiguration _configuration; public static MapperConfiguration Configure() { lock (Lock) { if (_configuration != null) return _configuration; var thisType = typeof(AutoMapperConfigurator); var configInterfaceType = typeof(IAutoMapperTypeConfigurator); var configurators = thisType.GetTypeInfo().Assembly.GetTypes() .Where(x => !string.IsNullOrWhiteSpace(x.Namespace)) // ReSharper disable once AssignNullToNotNullAttribute .Where(x => x.Namespace.Contains(thisType.Namespace)) .Where(x => x.GetTypeInfo().GetInterface(configInterfaceType.Name) != null) .Select(x => (IAutoMapperTypeConfigurator)Activator.CreateInstance(x)) .ToArray(); void AggregatedConfigurator(IMapperConfigurationExpression config) { foreach (var configurator in configurators) { configurator.Configure(config); } } _configuration = new MapperConfiguration(AggregatedConfigurator); return _configuration; } } }
Όλες οι αντιστοιχίσεις πρέπει να εφαρμόζουν μια συγκεκριμένη διεπαφή:
public interface IAutoMapperTypeConfigurator { void Configure(IMapperConfigurationExpression configuration); }
Ένα παράδειγμα χαρτογράφησης από οντότητα σε μοντέλο:
public class ExpenseMap : IAutoMapperTypeConfigurator { public void Configure(IMapperConfigurationExpression configuration) { var map = configuration.CreateMap(); map.ForMember(x => x.Username, x => x.MapFrom(y => y.User.FirstName + ' ' + y.User.LastName)); } }
Επίσης, στο Startup.ConfigureServices
μέθοδος, ο έλεγχος ταυτότητας μέσω διακριτικών JWT Bearer έχει ρυθμιστεί:
services.AddAuthorization(auth => { auth.AddPolicy('Bearer', new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser().Build()); });
Και οι υπηρεσίες κατέγραψαν την εφαρμογή ISecurityContext
, η οποία θα χρησιμοποιηθεί για τον προσδιορισμό του τρέχοντος χρήστη:
public class SecurityContext : ISecurityContext { private readonly IHttpContextAccessor _contextAccessor; private readonly IUnitOfWork _uow; private User _user; public SecurityContext(IHttpContextAccessor contextAccessor, IUnitOfWork uow) { _contextAccessor = contextAccessor; _uow = uow; } public User User { get { if (_user != null) return _user; var username = _contextAccessor.HttpContext.User.Identity.Name; _user = _uow.Query() .Where(x => x.Username == username) .Include(x => x.Roles) .ThenInclude(x => x.Role) .FirstOrDefault(); if (_user == null) { throw new UnauthorizedAccessException('User is not found'); } return _user; } } public bool IsAdministrator { get { return User.Roles.Any(x => x.Role.Name == Roles.Administrator); } } }
Επίσης, αλλάξαμε λίγο την προεπιλεγμένη καταχώριση MVC για να χρησιμοποιήσουμε ένα προσαρμοσμένο φίλτρο σφάλματος για να μετατρέψουμε εξαιρέσεις στους σωστούς κωδικούς σφάλματος:
services.AddMvc(options => { options.Filters.Add(new ApiExceptionFilter()); });
Υλοποίηση του ApiExceptionFilter
φίλτρο:
public class ApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { if (context.Exception is NotFoundException) { // handle explicit 'known' API errors var ex = context.Exception as NotFoundException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; } else if (context.Exception is BadRequestException) { // handle explicit 'known' API errors var ex = context.Exception as BadRequestException; context.Exception = null; context.Result = new JsonResult(ex.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else if (context.Exception is UnauthorizedAccessException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; } else if (context.Exception is ForbiddenException) { context.Result = new JsonResult(context.Exception.Message); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } base.OnException(context); } }
Είναι σημαντικό να μην ξεχνάτε Swagger
, για να λάβετε μια εξαιρετική περιγραφή API για άλλους https://www.toptal.com/api :
services.AddSwaggerGen(c => { c.SwaggerDoc('v1', new Info {Title = 'Expenses', Version = 'v1'}); c.OperationFilter(); });
Το Startup.Configure
Η μέθοδος προσθέτει μια κλήση στο InitDatabase
μέθοδο, η οποία μετεγκαθιστά αυτόματα τη βάση δεδομένων μέχρι την τελευταία μετεγκατάσταση:
private void InitDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) { var context = serviceScope.ServiceProvider.GetService(); context.Database.Migrate(); } }
Swagger
είναι ενεργοποιημένο μόνο εάν η εφαρμογή εκτελείται στο περιβάλλον ανάπτυξης και δεν απαιτεί έλεγχο ταυτότητας για πρόσβαση σε αυτήν:
app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint('/swagger/v1/swagger.json', 'My API V1'); });
Στη συνέχεια, συνδέουμε τον έλεγχο ταυτότητας (μπορείτε να βρείτε λεπτομέρειες στο αποθετήριο):
ConfigureAuthentication(app);
Σε αυτό το σημείο, μπορείτε να εκτελέσετε δοκιμές ενοποίησης και να βεβαιωθείτε ότι όλα έχουν συνταχθεί, αλλά τίποτα δεν λειτουργεί και μεταβείτε στον ελεγκτή ExpensesController
.
Σημείωση: Όλοι οι ελεγκτές βρίσκονται στο φάκελο 'Έξοδα / Διακομιστής' και χωρίζονται υπό όρους σε δύο φακέλους: Ελεγκτές και RestApi. Στο φάκελο, οι ελεγκτές είναι ελεγκτές που λειτουργούν ως ελεγκτές στο παλιό καλό MVC - δηλαδή, επιστρέφουν τη σήμανση και στο RestApi, REST ελεγκτές.
Πρέπει να δημιουργήσετε την κατηγορία Expenses / Server / RestApi / ExpensesController και να την κληρονομήσετε από την κλάση Controller:
public class ExpensesController : Controller { }
Στη συνέχεια, ρυθμίστε τη δρομολόγηση του ~ / api / Expenses
πληκτρολογήστε επισημαίνοντας την κλάση με το χαρακτηριστικό [Route ('api / [controller]')]
.
Για να αποκτήσετε πρόσβαση στη λογική της επιχείρησης και στο χαρτογράφο, πρέπει να κάνετε τις ακόλουθες υπηρεσίες:
private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; }
Σε αυτό το στάδιο, μπορείτε να ξεκινήσετε την εφαρμογή μεθόδων. Η πρώτη μέθοδος είναι να αποκτήσετε μια λίστα δαπανών:
[HttpGet] [QueryableResult] public IQueryable Get() { var result = _query.Get(); var models = _mapper.Map(result); return models; }
Η εφαρμογή της μεθόδου είναι πολύ απλή, παίρνουμε ένα ερώτημα στη βάση δεδομένων που έχει χαρτογραφηθεί στο IQueryable
από ExpensesQueryProcessor
, το οποίο με τη σειρά του επιστρέφει ως αποτέλεσμα.
Το προσαρμοσμένο χαρακτηριστικό εδώ είναι QueryableResult
, το οποίο χρησιμοποιεί το AutoQueryable
βιβλιοθήκη για χειρισμό σελιδοποίησης, φιλτραρίσματος και ταξινόμησης από την πλευρά του διακομιστή. Το χαρακτηριστικό βρίσκεται στο φάκελο Expenses/Filters
. Ως αποτέλεσμα, αυτό το φίλτρο επιστρέφει δεδομένα τύπου DataResult
στον πελάτη API.
public class QueryableResult : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Exception != null) return; dynamic query = ((ObjectResult)context.Result).Value; if (query == null) throw new Exception('Unable to retreive value of IQueryable from context result.'); Type entityType = query.GetType().GenericTypeArguments[0]; var commands = context.HttpContext.Request.Query.ContainsKey('commands') ? context.HttpContext.Request.Query['commands'] : new StringValues(); var data = QueryableHelper.GetAutoQuery(commands, entityType, query, new AutoQueryableProfile {UnselectableProperties = new string[0]}); var total = System.Linq.Queryable.Count(query); context.Result = new OkObjectResult(new DataResult{Data = data, Total = total}); } }
Ας δούμε επίσης την εφαρμογή της μεθόδου Post, δημιουργώντας μια ροή:
[HttpPost] [ValidateModel] public async Task Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map(item); return model; }
Εδώ, πρέπει να δώσετε προσοχή στο χαρακτηριστικό ValidateModel
, το οποίο εκτελεί απλή επικύρωση των δεδομένων εισαγωγής σύμφωνα με τα χαρακτηριστικά σχολιασμού δεδομένων και αυτό γίνεται μέσω των ενσωματωμένων ελέγχων MVC.
public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } }
Πλήρης κωδικός ExpensesController
:
[Route('api/[controller]')] public class ExpensesController : Controller { private readonly IExpensesQueryProcessor _query; private readonly IAutoMapper _mapper; public ExpensesController(IExpensesQueryProcessor query, IAutoMapper mapper) { _query = query; _mapper = mapper; } [HttpGet] [QueryableResult] public IQueryable Get() { var result = _query.Get(); var models = _mapper.Map(result); return models; } [HttpGet('{id}')] public ExpenseModel Get(int id) { var item = _query.Get(id); var model = _mapper.Map(item); return model; } [HttpPost] [ValidateModel] public async Task Post([FromBody]CreateExpenseModel requestModel) { var item = await _query.Create(requestModel); var model = _mapper.Map(item); return model; } [HttpPut('{id}')] [ValidateModel] public async Task Put(int id, [FromBody]UpdateExpenseModel requestModel) { var item = await _query.Update(id, requestModel); var model = _mapper.Map(item); return model; } [HttpDelete('{id}')] public async Task Delete(int id) { await _query.Delete(id); } }
Θα ξεκινήσω με προβλήματα: Το κύριο πρόβλημα είναι η πολυπλοκότητα της αρχικής διαμόρφωσης της λύσης και η κατανόηση των επιπέδων της εφαρμογής, αλλά με την αυξανόμενη πολυπλοκότητα της εφαρμογής, η πολυπλοκότητα του συστήματος είναι σχεδόν αμετάβλητη, κάτι που είναι μεγάλο συν όταν συνοδεύει ένα τέτοιο σύστημα. Και είναι πολύ σημαντικό να έχουμε ένα API για το οποίο υπάρχει ένα σύνολο δοκιμών ενοποίησης και ένα πλήρες σύνολο δοκιμών μονάδων για επιχειρηματική λογική. Η επιχειρησιακή λογική διαχωρίζεται εντελώς από την τεχνολογία διακομιστή που χρησιμοποιείται και μπορεί να δοκιμαστεί πλήρως. Αυτή η λύση είναι κατάλληλη για συστήματα με πολύπλοκο API και πολύπλοκη επιχειρηματική λογική.
Αν θέλετε να δημιουργήσετε μια γωνιακή εφαρμογή που καταναλώνει το API σας, ρίξτε μια ματιά Γωνιακός πυρήνας 5 και ASP.NET από τους συναδέλφους ApeeScapeer Pablo Albella.
Ένα αντικείμενο μεταφοράς δεδομένων (DTO) είναι μια αναπαράσταση ενός ή περισσότερων αντικειμένων σε μια βάση δεδομένων. Μια μεμονωμένη οντότητα βάσης δεδομένων μπορεί να αναπαρασταθεί με ή χωρίς αριθμό DTO
Ένα διαδικτυακό API παρέχει μια διεπαφή στην επιχειρησιακή λογική ενός συστήματος πρόσβασης στη βάση δεδομένων και η υποκείμενη λογική ενσωματώνεται στο API.
Η πραγματική διεπαφή μέσω της οποίας οι πελάτες μπορούν να εργαστούν με ένα Web API. Λειτουργεί μόνο μέσω πρωτοκόλλου HTTP.
Ο έλεγχος μονάδας είναι ένα σύνολο μικρών, ειδικών, πολύ γρήγορων δοκιμών που καλύπτουν μια μικρή μονάδα κώδικα, π.χ. τάξεις. Σε αντίθεση με τις δοκιμές ενοποίησης, η δοκιμή μονάδας διασφαλίζει ότι όλες οι πτυχές της μονάδας δοκιμάζονται μεμονωμένα από άλλα στοιχεία της συνολικής εφαρμογής.
Ο έλεγχος ενοποίησης είναι ένα σύνολο δοκιμών σε ένα συγκεκριμένο τελικό σημείο API. Σε αντίθεση με τον έλεγχο μονάδας, η δοκιμή ενοποίησης ελέγχει ότι όλες οι μονάδες κώδικα που τροφοδοτούν το API λειτουργούν όπως αναμενόταν. Αυτές οι δοκιμές μπορεί να είναι πιο αργές από τις δοκιμές μονάδας.
Το ASP.NET Core είναι μια επανεγγραφή και η επόμενη γενιά του ASP.NET 4.x. Είναι πολλαπλής πλατφόρμας και συμβατή με κοντέινερ Windows, Linux και Docker.
s corp vs c corp vs partnership
Ένα διακριτικό JWT (JSON Web Token) είναι ένα ανώνυμο και υπογεγραμμένο αντικείμενο JSON που χρησιμοποιείται ευρέως σε σύγχρονες εφαρμογές Web & Mobile για την παροχή πρόσβασης σε ένα API. Αυτά τα διακριτικά περιέχουν τις δικές τους αξιώσεις και γίνονται αποδεκτά εφόσον η υπογραφή είναι έγκυρη.
Το Swagger είναι ένα έγγραφο που χρησιμοποιείται στη βιβλιοθήκη ένα REST API. Η ίδια η τεκμηρίωση μπορεί επίσης να χρησιμοποιηθεί για τη δημιουργία ενός πελάτη για το API για διαφορετικές πλατφόρμες, αυτόματα.