Маршрутизация атрибутов включена, согласно скриншоту, опубликованному в комментариях, поскольку WebApiConfig
имеет конфигурацию по умолчанию
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
// Attribute routing.
config.MapHttpAttributeRoutes();
// Convention-based routing.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Обратите внимание на префикс api
на маршруте на основе соглашения.
Запрос от клиента делается на /SiteCollections/CreateModernSite
, который не соответствует Web API, так как контроллер API, похоже, не использует маршрутизацию атрибута, а запрошенный URL не соответствует маршруту на основе соглашения Web API.
Также на стороне клиента в опциях создается тело JSON, в то время как тип контента установлен на 'multipart/form-data'
Если целью является размещение содержимого в теле сообщения, то на стороне сервера вам потребуется внести несколько изменений, чтобы сделать API доступным.
[Authorize]
[RoutePrefix("api/SiteCollections")]
public class SiteCollectionsController : ApiController {
// GET api/SiteCollections
[HttpGet]
[Route("")]
public async Task<IHttpActionResult> Get() {
var tenant = await TenantHelper.GetTenantAsync();
using (var cc = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
var tenantOnline = new Tenant(cc);
SPOSitePropertiesEnumerable siteProps = tenantOnline.GetSitePropertiesFromSharePoint("0", true);
cc.Load(siteProps);
cc.ExecuteQuery();
var sites = siteProps.Select(site =>
new TenantManagementWebApi.Entities.SiteCollection() {
Url = site.Url,
Owner = site.Owner,
Template = site.Template,
Title = site.Title
})
.ToList();
return Ok(sites);
}
}
// POST api/SiteCollections
[HttpPost]
[Route("")]
public async Task<IHttpActionResult> CreateModernSite([FromBody]NewSiteInformation model) {
if(ModelState.IsValid) {
var tenant = await TenantHelper.GetTenantAsync();
using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
var teamContext = await context.CreateSiteAsync(
new TeamSiteCollectionCreationInformation {
Alias = model.Alias, // Mandatory
DisplayName = model.DisplayName, // Mandatory
Description = model.Description, // Optional
//Classification = Classification, // Optional
//IsPublic = IsPublic, // Optional, default true
}
);
teamContext.Load(teamContext.Web, _ => _.Url);
teamContext.ExecuteQueryRetry();
//204 with location and content set to created URL
return Created(teamContext.Web.Url, teamContext.Web.Url);
}
}
return BadRequest(ModelState);
}
public class NewSiteInformation {
[Required]
public string Alias { get; set; }
[Required]
public string DisplayName { get; set; }
public string Description { get; set; }
//...
}
}
Обратите внимание на включение правильной строго типизированной объектной модели для действия POST, проверки модели и возврата правильного кода состояния HTTP, как ожидается клиентом. (204)
На стороне клиента обновите вызываемый URL-адрес, чтобы он соответствовал маршруту контроллера API, и обновите параметры для отправки правильного типа контента.
//...
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(
{
Alias: this.state.Alias,
DisplayName: this.state.DisplayName,
Description: this.state.Description
})
};
adalApiFetch(fetch, "api/SiteCollections", options)
.then(response =>{
if(response.status === 204){
Notification(
'success',
'Site collection created',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Site collection not created',
error
);
console.error(error);
});
//...
Обратите внимание, что headers
находится непосредственно в параметрах выборки, как config.headers
в исходном коде.